#include #include #include #include #include #include #include #include #include #include #include #pragma comment(lib, "ws2_32.lib") // Atomic helpers static inline LONG AtomicRead(LONG* val) { return InterlockedCompareExchange(val, 0, 0); } static inline void AtomicWrite(LONG* val, LONG newVal) { InterlockedExchange(val, newVal); } // Async callback queue static std::vector> g_callbackQueue; static CRITICAL_SECTION g_callbackQueueCs; static void InitCallbackQueue() { InitializeCriticalSection(&g_callbackQueueCs); } static void ShutdownCallbackQueue() { DeleteCriticalSection(&g_callbackQueueCs); } static void QueueCallback(std::function callback) { EnterCriticalSection(&g_callbackQueueCs); g_callbackQueue.push_back(std::move(callback)); LeaveCriticalSection(&g_callbackQueueCs); } static void DrainCallbackQueue() { EnterCriticalSection(&g_callbackQueueCs); std::vector> localQueue; localQueue.swap(g_callbackQueue); LeaveCriticalSection(&g_callbackQueueCs); for (auto& cb : localQueue) { if (cb) cb(); } } // logging system FILE *logFile = NULL; FILE *packetFile = NULL; static CRITICAL_SECTION g_packetCs; void InitLogFiles(); void CloseLogFiles(); void Log(const char *format, ...) { if (!logFile) return; va_list args; va_start(args, format); SYSTEMTIME st; GetLocalTime(&st); fprintf(logFile, "[%02u:%02u:%02u.%03u] ", st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); vfprintf(logFile, format, args); fprintf(logFile, "\n"); va_end(args); } static void HexDump(const void *data, size_t len, char *out) { const unsigned char *p = (const unsigned char *)data; for (size_t i = 0; i < len; ++i) sprintf(out + i * 3, "%02X ", p[i]); if (len > 0) out[len * 3 - 1] = '\0'; } typedef USHORT(WINAPI *RtlCaptureStackBackTraceFn)(ULONG, ULONG, PVOID *, PULONG); static RtlCaptureStackBackTraceFn pRtlCaptureStackBackTrace = nullptr; static void LogCallStack(const char *context) { if (!pRtlCaptureStackBackTrace) { HMODULE ntdll = GetModuleHandleA("ntdll.dll"); if (ntdll) { pRtlCaptureStackBackTrace = (RtlCaptureStackBackTraceFn)GetProcAddress(ntdll, "RtlCaptureStackBackTrace"); } if (!pRtlCaptureStackBackTrace) { Log("[Stack/%s] !! RtlCaptureStackBackTrace unavailable", context); return; } } void *frames[16] = {0}; USHORT count = pRtlCaptureStackBackTrace(1, 16, frames, nullptr); char buf[2048] = {0}; int pos = 0; for (USHORT i = 0; i < count && pos < (int)sizeof(buf) - 128; i++) { HMODULE mod = nullptr; if (GetModuleHandleExA(0x4 /*FROM_ADDRESS*/ | 0x2 /*UNCHANGED_REFCOUNT*/, (LPCSTR)frames[i], &mod) && mod) { char modPath[MAX_PATH] = {0}; GetModuleFileNameA(mod, modPath, MAX_PATH); const char *modName = strrchr(modPath, '\\'); modName = modName ? modName + 1 : modPath; pos += sprintf_s(buf + pos, sizeof(buf) - pos, "%s+0x%llx ", modName, (unsigned long long)((uintptr_t)frames[i] - (uintptr_t)mod)); } else { pos += sprintf_s(buf + pos, sizeof(buf) - pos, "0x%p ", frames[i]); } } Log("[Stack/%s] %s", context, buf); } static bool StackContainsModule(const char *targetModule) { if (!pRtlCaptureStackBackTrace) { HMODULE ntdll = GetModuleHandleA("ntdll.dll"); if (ntdll) { pRtlCaptureStackBackTrace = (RtlCaptureStackBackTraceFn)GetProcAddress(ntdll, "RtlCaptureStackBackTrace"); } if (!pRtlCaptureStackBackTrace) return true; } void *frames[16] = {0}; USHORT count = pRtlCaptureStackBackTrace(1, 16, frames, nullptr); for (USHORT i = 0; i < count; i++) { HMODULE mod = nullptr; if (GetModuleHandleExA(0x4 /*FROM_ADDRESS*/ | 0x2 /*UNCHANGED_REFCOUNT*/, (LPCSTR)frames[i], &mod) && mod) { char modPath[MAX_PATH] = {0}; GetModuleFileNameA(mod, modPath, MAX_PATH); const char *modName = strrchr(modPath, '\\'); modName = modName ? modName + 1 : modPath; if (_stricmp(modName, targetModule) == 0) return true; } } return false; } static void AsciiDump(const void *data, size_t len, char *out) { const unsigned char *p = (const unsigned char *)data; for (size_t i = 0; i < len; ++i) out[i] = (p[i] >= 32 && p[i] < 127) ? (char)p[i] : '.'; out[len] = '\0'; } void DumpPacket(const char *direction, const void *data, uint32_t len, const struct sockaddr_in *addr, uint8_t channel) { if (!packetFile || !data || len == 0) return; EnterCriticalSection(&g_packetCs); SYSTEMTIME st; GetLocalTime(&st); fprintf(packetFile, "\n========== PACKET %s [%02u:%02u:%02u.%03u] ==========\n", direction, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); if (addr) fprintf(packetFile, "Peer: %s:%d ", inet_ntoa(addr->sin_addr), ntohs(addr->sin_port)); fprintf(packetFile, "Size: %u bytes Channel: %u\n", len, channel); const unsigned char *p = (const unsigned char *)data; for (uint32_t offset = 0; offset < len; offset += 16) { uint32_t lineLen = (len - offset) > 16 ? 16 : (len - offset); char hex[16 * 3 + 1] = {0}; char ascii[16 + 1] = {0}; HexDump(p + offset, lineLen, hex); AsciiDump(p + offset, lineLen, ascii); fprintf(packetFile, "%04X %-48s %s\n", offset, hex, ascii); } fprintf(packetFile, "==================================================\n"); fflush(packetFile); LeaveCriticalSection(&g_packetCs); } // Forward declarations void InitNetwork(); void LogStateSnapshot(const char *context); void ResetConnectionState(); void FirePeerConnectionEstablished(void* remotePeerId); void FirePeerConnectionRequest(void* remotePeerId); void FireLobbyMemberJoined(void* memberId); void HostOnFirstPacketReceived(const struct sockaddr_in* from); void SendHelloAck(const struct sockaddr_in* to); void TryFireHostEvents(void* clientId); void TickRetryPendingEvents(); static void SendHelloAckLocked(const struct sockaddr_in* to); static void HostOnFirstPacketReceivedNoLock(const struct sockaddr_in* from); static bool DiscardMitmHelloIfPresentLocked(); // Network State SOCKET udpSocket = INVALID_SOCKET; struct sockaddr_in targetAddr; static LONG g_isHost = 0; static LONG g_initialized = 0; char g_dllDir[MAX_PATH] = {0}; char g_searchAppVersion[64] = "1.2.0"; char g_searchOnlineRule[64] = "Koikoi"; char g_lobbyAppVersion[64] = "1.2.0"; char g_lobbyOnlineRule[64] = "Koikoi"; char g_lobbyPlatform[32] = "Steam"; char g_lobbyEosProductId[64] = "DIRECTIP_HOST_PUID"; int64_t g_lobbyWinRate = 10; bool g_lobbyIsCrossPlatform = true; static LONG g_clientSawSearchResult = 0; static uint32_t g_maxMembers = 4; const char *kMitmHello = "EOSMITM_HELLO"; static const char *kMitmAck = "EOSMITM_ACK"; // Per-session log throttle counters (reset in ResetConnectionState) static LONG g_sendLogCount = 0; static LONG g_receiveLogCount = 0; static LONG g_sizeLogCount = 0; static LONG g_copyInfoLogCount = 0; static LONG g_ownerLogCount = 0; static LONG g_memberByIndexLogCount = 0; static LONG g_memberCountLogCount = 0; static LONG g_memberInfoLogCount = 0; static LONG g_peerConnected = 0; static LONG g_lobbyHasRemoteMember = 0; static LONG g_hostFiredEvents = 0; static LONG g_helloAckReceived = 0; static LONG g_hostHasClientAddr = 0; static LONG g_establishedFired = 0; static LONG g_lobbyMemberJoinedFired = 0; static LONG g_connectionRequestFired = 0; static LONG g_initLock = 0; typedef void (*EOS_Connect_OnLoginStatusChangedCallback)(const void* Data); static EOS_Connect_OnLoginStatusChangedCallback g_loginStatusChangedCb = nullptr; static void* g_loginStatusChangedCbData = nullptr; static CRITICAL_SECTION g_hostEventCs; static CRITICAL_SECTION g_socketCs; static HANDLE g_stopEvent = NULL; static HANDLE g_peerWatchThread = NULL; static uint64_t g_notifIdCounter = 10; static LONG g_watchThreadGeneration = 0; // Rate limiting for ResetConnectionState static DWORD g_lastResetTime = 0; static volatile DWORD g_lastGoodPacketTime = 0; // GetTickCount() of last successful send/recv, for CloseConnections health-gating bodge static LONG g_resetCount = 0; static LONG g_resetInProgress = 0; static const DWORD kAutoTeardownSuppressWindowMs = 100; static LONG g_autoTeardownSuppressed = 0; static volatile DWORD g_autoTeardownSuppressTime = 0; void LogStateSnapshot(const char *context) { Log("[State] %s | isHost=%d peerConnected=%d lobbyHasRemoteMember=%d initialized=%d clientSawSearchResult=%d", context ? context : "???", (int)AtomicRead(&g_isHost), (int)AtomicRead(&g_peerConnected), (int)AtomicRead(&g_lobbyHasRemoteMember), (int)AtomicRead(&g_initialized), (int)AtomicRead(&g_clientSawSearchResult)); } // EOS Types Mock typedef int32_t EOS_EResult; #define EOS_Success 0 #define EOS_NotFound 8 #define EOS_InvalidParameters 4 typedef void *EOS_HP2P; typedef void *EOS_ProductUserId; struct EOS_P2P_SocketId { int32_t ApiVersion; char SocketName[33]; }; static EOS_P2P_SocketId g_socketId = { 2, "DirectIP" }; static EOS_ProductUserId HostProductUserId() { return (EOS_ProductUserId)1; } static EOS_ProductUserId ClientProductUserId() { return (EOS_ProductUserId)2; } static EOS_ProductUserId LocalProductUserId() { InitNetwork(); return AtomicRead(&g_isHost) ? HostProductUserId() : ClientProductUserId(); } static EOS_ProductUserId RemoteProductUserId() { InitNetwork(); return AtomicRead(&g_isHost) ? ClientProductUserId() : HostProductUserId(); } // P2P structs struct EOS_P2P_SendPacketOptions { int32_t ApiVersion; EOS_ProductUserId LocalUserId; EOS_ProductUserId RemoteUserId; const void *SocketId; uint8_t Channel; uint32_t DataLengthBytes; const void *Data; bool bAllowDelayedDelivery; uint8_t Reliability; bool bDisableAutoAcceptConnection; }; struct EOS_P2P_GetNextReceivedPacketSizeOptions { int32_t ApiVersion; EOS_ProductUserId LocalUserId; const uint8_t *RequestedChannel; }; struct EOS_P2P_ReceivePacketOptions { int32_t ApiVersion; EOS_ProductUserId LocalUserId; uint32_t MaxDataSizeBytes; const uint8_t *RequestedChannel; }; // Callback types struct EOS_P2P_OnIncomingConnectionRequestInfo { void* ClientData; EOS_ProductUserId LocalUserId; EOS_ProductUserId RemoteUserId; const EOS_P2P_SocketId* SocketId; }; typedef void (*EOS_P2P_OnIncomingConnectionRequestCallback)(const EOS_P2P_OnIncomingConnectionRequestInfo* Data); struct EOS_P2P_OnPeerConnectionEstablishedInfo { void* ClientData; EOS_ProductUserId LocalUserId; EOS_ProductUserId RemoteUserId; const EOS_P2P_SocketId* SocketId; int32_t ConnectionType; }; typedef void (*EOS_P2P_OnPeerConnectionEstablishedCallback)(const EOS_P2P_OnPeerConnectionEstablishedInfo* Data); static EOS_P2P_OnIncomingConnectionRequestCallback g_connectionRequestCb = nullptr; static void* g_connectionRequestCbData = nullptr; static EOS_P2P_OnPeerConnectionEstablishedCallback g_connectionEstablishedCb = nullptr; static void* g_connectionEstablishedCbData = nullptr; struct EOS_Lobby_LobbyMemberStatusReceivedCallbackInfo { void* ClientData; const char* LobbyId; EOS_ProductUserId TargetUserId; int32_t CurrentStatus; }; typedef void (*EOS_Lobby_OnLobbyMemberStatusReceivedCallback)(const EOS_Lobby_LobbyMemberStatusReceivedCallbackInfo* Data); static EOS_Lobby_OnLobbyMemberStatusReceivedCallback g_memberStatusCb = nullptr; static void* g_memberStatusCbData = nullptr; struct EOS_Lobby_LobbyUpdateReceivedCallbackInfo { void* ClientData; const char* LobbyId; }; typedef void (*EOS_Lobby_OnLobbyUpdateReceivedCallback)(const EOS_Lobby_LobbyUpdateReceivedCallbackInfo* Data); struct EOS_Lobby_LobbyMemberUpdateReceivedCallbackInfo { void* ClientData; const char* LobbyId; EOS_ProductUserId TargetUserId; }; typedef void (*EOS_Lobby_OnLobbyMemberUpdateReceivedCallback)(const EOS_Lobby_LobbyMemberUpdateReceivedCallbackInfo* Data); static EOS_Lobby_OnLobbyUpdateReceivedCallback g_lobbyUpdateCb = nullptr; static void* g_lobbyUpdateCbData = nullptr; static EOS_Lobby_OnLobbyMemberUpdateReceivedCallback g_memberUpdateCb = nullptr; static void* g_memberUpdateCbData = nullptr; void ResetConnectionState() { DWORD now = GetTickCount(); bool nothingToReset = (udpSocket == INVALID_SOCKET) && !g_peerWatchThread && AtomicRead(&g_initialized) == 0 && AtomicRead(&g_peerConnected) == 0 && AtomicRead(&g_lobbyHasRemoteMember) == 0; if (nothingToReset && (now - g_lastResetTime) < 50) { Log("[Net] ResetConnectionState: already clean, skipping redundant call"); return; } g_lastResetTime = now; LogCallStack("ResetConnectionState"); // Clear auto-teardown suppression: any real reset cancels the window AtomicWrite(&g_autoTeardownSuppressed, 0); AtomicWrite(&g_resetInProgress, 1); Log("[Net] Resetting connection state for new session"); if (g_peerWatchThread) { InterlockedIncrement(&g_watchThreadGeneration); if (g_stopEvent) SetEvent(g_stopEvent); WaitForSingleObject(g_peerWatchThread, 50); // best-effort; not relied upon below EnterCriticalSection(&g_socketCs); CloseHandle(g_peerWatchThread); g_peerWatchThread = NULL; LeaveCriticalSection(&g_socketCs); } // Reset per-session log throttle counters so sessions 4+ remain visible. AtomicWrite(&g_sendLogCount, 0); AtomicWrite(&g_receiveLogCount, 0); AtomicWrite(&g_sizeLogCount, 0); AtomicWrite(&g_copyInfoLogCount, 0); AtomicWrite(&g_ownerLogCount, 0); AtomicWrite(&g_memberByIndexLogCount, 0); AtomicWrite(&g_memberCountLogCount, 0); AtomicWrite(&g_memberInfoLogCount, 0); EnterCriticalSection(&g_socketCs); if (udpSocket != INVALID_SOCKET) { closesocket(udpSocket); udpSocket = INVALID_SOCKET; } LeaveCriticalSection(&g_socketCs); AtomicWrite(&g_peerConnected, 0); AtomicWrite(&g_lobbyHasRemoteMember, 0); AtomicWrite(&g_hostFiredEvents, 0); AtomicWrite(&g_clientSawSearchResult, 0); AtomicWrite(&g_helloAckReceived, 0); AtomicWrite(&g_hostHasClientAddr, 0); AtomicWrite(&g_establishedFired, 0); AtomicWrite(&g_lobbyMemberJoinedFired, 0); AtomicWrite(&g_connectionRequestFired, 0); AtomicWrite(&g_initialized, 0); AtomicWrite(&g_isHost, 0); g_connectionRequestCb = nullptr; g_connectionRequestCbData = nullptr; g_connectionEstablishedCb = nullptr; g_connectionEstablishedCbData = nullptr; g_memberStatusCb = nullptr; g_memberStatusCbData = nullptr; g_lobbyUpdateCb = nullptr; g_lobbyUpdateCbData = nullptr; g_memberUpdateCb = nullptr; g_memberUpdateCbData = nullptr; AtomicWrite(&g_resetInProgress, 0); } void InitNetwork() { if (AtomicRead(&g_initialized)) return; if (InterlockedCompareExchange(&g_initLock, 1, 0) != 0) { while (!AtomicRead(&g_initialized)) Sleep(1); return; } if (AtomicRead(&g_initialized)) { InterlockedExchange(&g_initLock, 0); return; } WSADATA wsaData; int wsaErr = WSAStartup(MAKEWORD(2, 2), &wsaData); Log("[Net] WSAStartup result: %d", wsaErr); char iniPath[MAX_PATH]; snprintf(iniPath, sizeof(iniPath), "%sdirectip.ini", g_dllDir); char role[32] = {0}; char ip[64] = {0}; char portStr[16] = {0}; GetPrivateProfileStringA("Network", "Role", "Client", role, sizeof(role), iniPath); GetPrivateProfileStringA("Network", "IP", "127.0.0.1", ip, sizeof(ip), iniPath); GetPrivateProfileStringA("Network", "Port", "7777", portStr, sizeof(portStr), iniPath); int port = atoi(portStr); AtomicWrite(&g_isHost, (_stricmp(role, "Host") == 0) ? 1 : 0); EnterCriticalSection(&g_socketCs); udpSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udpSocket == INVALID_SOCKET) { Log("[Net] socket() failed: %d", WSAGetLastError()); LeaveCriticalSection(&g_socketCs); InterlockedExchange(&g_initLock, 0); return; } // Allow rapid rebind to avoid WSAEADDRINUSE int reuse = 1; if (setsockopt(udpSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&reuse, sizeof(reuse)) == SOCKET_ERROR) { Log("[Net] setsockopt(SO_REUSEADDR) failed: %d", WSAGetLastError()); } u_long mode = 1; ioctlsocket(udpSocket, FIONBIO, &mode); struct sockaddr_in bindAddr = {0}; bindAddr.sin_family = AF_INET; bindAddr.sin_addr.s_addr = INADDR_ANY; bindAddr.sin_port = htons(AtomicRead(&g_isHost) ? port : 0); int bindRes = bind(udpSocket, (struct sockaddr *)&bindAddr, sizeof(bindAddr)); LeaveCriticalSection(&g_socketCs); Log("[Net] bind() result: %d (port %d)", bindRes, AtomicRead(&g_isHost) ? port : 0); if (bindRes != 0) { Log("[Net] bind() failed, aborting network init"); EnterCriticalSection(&g_socketCs); if (udpSocket != INVALID_SOCKET) { closesocket(udpSocket); udpSocket = INVALID_SOCKET; } LeaveCriticalSection(&g_socketCs); InterlockedExchange(&g_initLock, 0); return; } targetAddr.sin_family = AF_INET; targetAddr.sin_port = htons(port); targetAddr.sin_addr.s_addr = inet_addr(ip); // Read optional lobby attributes from ini char maxMembersStr[16] = "4"; GetPrivateProfileStringA("Network", "MaxMembers", "4", maxMembersStr, sizeof(maxMembersStr), iniPath); g_maxMembers = (uint32_t)atoi(maxMembersStr); char platformStr[32] = "Steam"; GetPrivateProfileStringA("Network", "Platform", "Steam", platformStr, sizeof(platformStr), iniPath); strncpy(g_lobbyPlatform, platformStr, sizeof(g_lobbyPlatform) - 1); g_lobbyPlatform[sizeof(g_lobbyPlatform) - 1] = '\0'; char winRateStr[32] = "10"; GetPrivateProfileStringA("Network", "WinRate", "10", winRateStr, sizeof(winRateStr), iniPath); g_lobbyWinRate = _atoi64(winRateStr); char crossStr[16] = "true"; GetPrivateProfileStringA("Network", "IsCrossPlatform", "true", crossStr, sizeof(crossStr), iniPath); g_lobbyIsCrossPlatform = (_stricmp(crossStr, "true") == 0 || strcmp(crossStr, "1") == 0); char appVerStr[64] = "1.2.0"; GetPrivateProfileStringA("Network", "AppVersion", "1.2.0", appVerStr, sizeof(appVerStr), iniPath); strncpy(g_lobbyAppVersion, appVerStr, sizeof(g_lobbyAppVersion) - 1); g_lobbyAppVersion[sizeof(g_lobbyAppVersion) - 1] = '\0'; char ruleStr[64] = "Koikoi"; GetPrivateProfileStringA("Network", "OnlineRule", "Koikoi", ruleStr, sizeof(ruleStr), iniPath); strncpy(g_lobbyOnlineRule, ruleStr, sizeof(g_lobbyOnlineRule) - 1); g_lobbyOnlineRule[sizeof(g_lobbyOnlineRule) - 1] = '\0'; Log("[Net] Initialized. Role=%s Target=%s:%d BindPort=%d ini=%s", role, ip, port, AtomicRead(&g_isHost) ? port : 0, iniPath); LogStateSnapshot("InitNetwork"); AtomicWrite(&g_initialized, 1); InterlockedExchange(&g_initLock, 0); } void FirePeerConnectionEstablished(EOS_ProductUserId remotePeerId) { Log("[Net] >> FirePeerConnectionEstablished remote=%p", remotePeerId); if (!g_connectionEstablishedCb) { Log("[Net] g_connectionEstablishedCb is NULL, cannot fire yet"); return; } if (InterlockedCompareExchange(&g_establishedFired, 1, 0) != 0) { Log("[Net] FirePeerConnectionEstablished already delivered, skipping"); return; } EOS_P2P_OnPeerConnectionEstablishedInfo info = {0}; info.ClientData = g_connectionEstablishedCbData; info.LocalUserId = LocalProductUserId(); info.RemoteUserId = remotePeerId; info.SocketId = &g_socketId; info.ConnectionType = 0; Log("[Net] Invoking g_connectionEstablishedCb"); g_connectionEstablishedCb(&info); Log("[Net] << FirePeerConnectionEstablished"); } void FirePeerConnectionRequest(EOS_ProductUserId remotePeerId) { Log("[Net] >> FirePeerConnectionRequest remote=%p", remotePeerId); AtomicWrite(&g_peerConnected, 1); AtomicWrite(&g_lobbyHasRemoteMember, 1); if (g_connectionRequestCb) { if (InterlockedCompareExchange(&g_connectionRequestFired, 1, 0) != 0) { Log("[Net] FirePeerConnectionRequest already delivered, skipping"); return; } LogStateSnapshot("FirePeerConnectionRequest"); EOS_P2P_OnIncomingConnectionRequestInfo info = {0}; info.ClientData = g_connectionRequestCbData; info.LocalUserId = LocalProductUserId(); info.RemoteUserId = remotePeerId; info.SocketId = &g_socketId; Log("[Net] Invoking g_connectionRequestCb"); g_connectionRequestCb(&info); // Do not auto-fire established here. The game must call AcceptConnection. } else { Log("[Net] g_connectionRequestCb is NULL, will retry when registered"); } Log("[Net] << FirePeerConnectionRequest"); } void FireLobbyMemberJoined(EOS_ProductUserId memberId) { Log("[Net] >> FireLobbyMemberJoined member=%p", memberId); AtomicWrite(&g_lobbyHasRemoteMember, 1); if (!g_memberStatusCb) { Log("[Net] g_memberStatusCb is NULL, cannot fire yet"); return; } if (InterlockedCompareExchange(&g_lobbyMemberJoinedFired, 1, 0) != 0) { Log("[Net] FireLobbyMemberJoined already delivered, skipping"); return; } EOS_Lobby_LobbyMemberStatusReceivedCallbackInfo info = {0}; info.ClientData = g_memberStatusCbData; info.LobbyId = "DirectIP_Lobby"; info.TargetUserId = memberId; info.CurrentStatus = 0; Log("[Net] Invoking g_memberStatusCb for JOINED"); g_memberStatusCb(&info); Log("[Net] << FireLobbyMemberJoined"); } static void SendHelloAckLocked(const struct sockaddr_in* to) { if (udpSocket == INVALID_SOCKET || !to) return; sendto(udpSocket, kMitmAck, (int)strlen(kMitmAck), 0, (struct sockaddr*)to, sizeof(*to)); Log("[Net] Sent ACK to %s:%d", inet_ntoa(to->sin_addr), ntohs(to->sin_port)); } void SendHelloAck(const struct sockaddr_in* to) { if (udpSocket == INVALID_SOCKET || !to) return; EnterCriticalSection(&g_socketCs); SendHelloAckLocked(to); LeaveCriticalSection(&g_socketCs); } static void HostOnFirstPacketReceivedNoLock(const struct sockaddr_in* from) { if (!from) return; if (!AtomicRead(&g_hostHasClientAddr)) { targetAddr = *from; AtomicWrite(&g_hostHasClientAddr, 1); } Log("[Net] Host detected client at %s:%d — firing member+connection events", inet_ntoa(from->sin_addr), ntohs(from->sin_port)); LogStateSnapshot("HostFirstPacket"); TryFireHostEvents(ClientProductUserId()); } void HostOnFirstPacketReceived(const struct sockaddr_in* from) { if (!from) return; EnterCriticalSection(&g_socketCs); HostOnFirstPacketReceivedNoLock(from); LeaveCriticalSection(&g_socketCs); } void TryFireHostEvents(void* clientId) { EOS_ProductUserId id = (EOS_ProductUserId)clientId; if (!AtomicRead(&g_hostFiredEvents)) { EnterCriticalSection(&g_hostEventCs); if (!AtomicRead(&g_hostFiredEvents)) { AtomicWrite(&g_hostFiredEvents, 1); LeaveCriticalSection(&g_hostEventCs); AtomicWrite(&g_lobbyHasRemoteMember, 1); AtomicWrite(&g_peerConnected, 1); FireLobbyMemberJoined(id); FirePeerConnectionRequest(id); // Established is intentionally omitted; game must call AcceptConnection } else { LeaveCriticalSection(&g_hostEventCs); } } else { // Fallbacks for late-registered callbacks only if (AtomicRead(&g_lobbyHasRemoteMember) && g_memberStatusCb && InterlockedCompareExchange(&g_lobbyMemberJoinedFired, 1, 0) == 0) { FireLobbyMemberJoined(id); } if (AtomicRead(&g_peerConnected) && g_connectionRequestCb && InterlockedCompareExchange(&g_connectionRequestFired, 1, 0) == 0) { FirePeerConnectionRequest(id); } // Removed Established fallback to enforce AcceptConnection contract } } void TickRetryPendingEvents() { bool memberPending = AtomicRead(&g_lobbyHasRemoteMember) && AtomicRead(&g_lobbyMemberJoinedFired) == 0 && g_memberStatusCb; bool requestPending = AtomicRead(&g_peerConnected) && AtomicRead(&g_connectionRequestFired) == 0 && g_connectionRequestCb; if (!memberPending && !requestPending) return; EOS_ProductUserId id = RemoteProductUserId(); if (memberPending) FireLobbyMemberJoined(id); if (requestPending) FirePeerConnectionRequest(id); } static bool DiscardMitmHelloIfPresentLocked() { // Caller must hold g_socketCs if (udpSocket == INVALID_SOCKET) return false; char peekBuf[64]; struct sockaddr_in from; int fromLen = sizeof(from); int peeked = recvfrom(udpSocket, peekBuf, sizeof(peekBuf), MSG_PEEK, (struct sockaddr *)&from, &fromLen); bool result = false; size_t helloLen = strlen(kMitmHello); size_t ackLen = strlen(kMitmAck); if (peeked == (int)helloLen && memcmp(peekBuf, kMitmHello, helloLen) == 0) { recvfrom(udpSocket, peekBuf, sizeof(peekBuf), 0, (struct sockaddr *)&from, &fromLen); if (AtomicRead(&g_isHost)) { if (!AtomicRead(&g_hostHasClientAddr)) { targetAddr = from; AtomicWrite(&g_hostHasClientAddr, 1); } SendHelloAckLocked(&from); TryFireHostEvents(ClientProductUserId()); } Log("[Net] Filtered DirectIP hello from %s:%d", inet_ntoa(from.sin_addr), ntohs(from.sin_port)); result = true; } else if (!AtomicRead(&g_isHost) && peeked == (int)ackLen && memcmp(peekBuf, kMitmAck, ackLen) == 0) { recvfrom(udpSocket, peekBuf, sizeof(peekBuf), 0, (struct sockaddr *)&from, &fromLen); AtomicWrite(&g_helloAckReceived, 1); AtomicWrite(&g_peerConnected, 1); AtomicWrite(&g_lobbyHasRemoteMember, 1); Log("[Net] Filtered DirectIP ACK from %s:%d", inet_ntoa(from.sin_addr), ntohs(from.sin_port)); result = true; } return result; } DWORD WINAPI PeerWatchThread(LPVOID param) { LONG myGeneration = (LONG)(intptr_t)param; InitNetwork(); Log("[Thread] PeerWatchThread started. isHost=%d generation=%ld", (int)AtomicRead(&g_isHost), (long)myGeneration); if (AtomicRead(&g_isHost)) { int loopCount = 0; while (loopCount < 400) { if (WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) break; if (AtomicRead(&g_watchThreadGeneration) != myGeneration) { Log("[Thread] Host: stale generation, exiting"); break; } char peekBuf[64]; struct sockaddr_in from; int fromLen = sizeof(from); bool consumedHello = false; bool gotNonHello = false; EnterCriticalSection(&g_socketCs); u_long bytesAvail = 0; if (ioctlsocket(udpSocket, FIONREAD, &bytesAvail) == 0 && bytesAvail > 0) { int peeked = recvfrom(udpSocket, peekBuf, sizeof(peekBuf), MSG_PEEK, (struct sockaddr*)&from, &fromLen); if (peeked > 0) { size_t helloLen = strlen(kMitmHello); if (peeked == (int)helloLen && memcmp(peekBuf, kMitmHello, helloLen) == 0) { recvfrom(udpSocket, peekBuf, sizeof(peekBuf), 0, (struct sockaddr*)&from, &fromLen); consumedHello = true; } else { gotNonHello = true; } } } LeaveCriticalSection(&g_socketCs); if (consumedHello) { Log("[Net] Consumed DirectIP hello from client %s:%d", inet_ntoa(from.sin_addr), ntohs(from.sin_port)); EnterCriticalSection(&g_socketCs); if (!AtomicRead(&g_hostHasClientAddr)) { targetAddr = from; AtomicWrite(&g_hostHasClientAddr, 1); } LeaveCriticalSection(&g_socketCs); SendHelloAck(&from); TryFireHostEvents(ClientProductUserId()); continue; } if (gotNonHello) { if (!AtomicRead(&g_hostHasClientAddr)) { EnterCriticalSection(&g_socketCs); if (!AtomicRead(&g_hostHasClientAddr)) { targetAddr = from; AtomicWrite(&g_hostHasClientAddr, 1); } LeaveCriticalSection(&g_socketCs); } TryFireHostEvents(ClientProductUserId()); if (AtomicRead(&g_peerConnected) && AtomicRead(&g_lobbyHasRemoteMember)) { Log("[Thread] Host bootstrap complete, exiting watch thread"); break; } } else if (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected)) { TryFireHostEvents(ClientProductUserId()); if (AtomicRead(&g_peerConnected) && AtomicRead(&g_lobbyHasRemoteMember)) { Log("[Thread] Host bootstrap complete, exiting watch thread"); break; } } Sleep(50); if (++loopCount % 20 == 0) { Log("[Thread] Host waiting for client hello... (%d loops)", loopCount); } } } else { int helloRetry = 0; while (AtomicRead(&g_helloAckReceived) == 0 && AtomicRead(&g_peerConnected) == 0) { if (WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) break; if (AtomicRead(&g_watchThreadGeneration) != myGeneration) { Log("[Thread] Client hello: stale generation, exiting"); goto thread_exit; } EnterCriticalSection(&g_socketCs); sendto(udpSocket, kMitmHello, (int)strlen(kMitmHello), 0, (struct sockaddr *)&targetAddr, sizeof(targetAddr)); LeaveCriticalSection(&g_socketCs); Log("[Net] Client sent DirectIP hello to host (attempt %d)", helloRetry + 1); DWORD start = GetTickCount(); while (GetTickCount() - start < 250) { if (WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) break; char buf[64]; struct sockaddr_in from; int fromLen = sizeof(from); int r = -1; EnterCriticalSection(&g_socketCs); u_long bytesAvail = 0; if (ioctlsocket(udpSocket, FIONREAD, &bytesAvail) == 0 && bytesAvail > 0) { r = recvfrom(udpSocket, buf, sizeof(buf), MSG_PEEK, (struct sockaddr*)&from, &fromLen); } LeaveCriticalSection(&g_socketCs); if (r > 0) { size_t ackLen = strlen(kMitmAck); if (r == (int)ackLen && memcmp(buf, kMitmAck, ackLen) == 0) { EnterCriticalSection(&g_socketCs); recvfrom(udpSocket, buf, sizeof(buf), 0, (struct sockaddr*)&from, &fromLen); LeaveCriticalSection(&g_socketCs); AtomicWrite(&g_helloAckReceived, 1); AtomicWrite(&g_peerConnected, 1); AtomicWrite(&g_lobbyHasRemoteMember, 1); Log("[Net] Client received ACK from host %s:%d", inet_ntoa(from.sin_addr), ntohs(from.sin_port)); break; } else { AtomicWrite(&g_peerConnected, 1); AtomicWrite(&g_lobbyHasRemoteMember, 1); Log("[Net] Client received data from host, assuming connected"); break; } } Sleep(10); } if (AtomicRead(&g_helloAckReceived) || AtomicRead(&g_peerConnected)) break; helloRetry++; if (helloRetry > 40) { Log("[Net] Client gave up waiting for host after %d retries", helloRetry); break; } } int eventRetry = 0; while (eventRetry < 200) { if (WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) break; if (AtomicRead(&g_watchThreadGeneration) != myGeneration) { Log("[Thread] Client events: stale generation, exiting"); goto thread_exit; } bool needRequest = AtomicRead(&g_connectionRequestFired) == 0; bool needEstablished = AtomicRead(&g_establishedFired) == 0; bool needMember = AtomicRead(&g_lobbyMemberJoinedFired) == 0; if (!needRequest && !needEstablished && !needMember) { Log("[Thread] Client all events fired successfully"); break; } if (needMember && g_memberStatusCb) { FireLobbyMemberJoined(HostProductUserId()); } if (needRequest && g_connectionRequestCb) { FirePeerConnectionRequest(HostProductUserId()); } if (needEstablished && g_connectionEstablishedCb) { FirePeerConnectionEstablished(HostProductUserId()); } Sleep(100); eventRetry++; } } thread_exit: Log("[Thread] PeerWatchThread exiting (generation=%ld)", (long)myGeneration); EnterCriticalSection(&g_socketCs); if (AtomicRead(&g_watchThreadGeneration) == myGeneration) { if (g_peerWatchThread) CloseHandle(g_peerWatchThread); g_peerWatchThread = NULL; } LeaveCriticalSection(&g_socketCs); return 0; } static bool ShouldSuppressAutoTeardown() { if (!AtomicRead(&g_autoTeardownSuppressed)) return false; DWORD now = GetTickCount(); DWORD elapsed = now - g_autoTeardownSuppressTime; if (elapsed < kAutoTeardownSuppressWindowMs) { Log("[Proxy] ShouldSuppressAutoTeardown: active (%lu ms elapsed), " "deferring reset", (unsigned long)elapsed); return true; } Log("[Proxy] ShouldSuppressAutoTeardown: window expired (%lu ms elapsed), " "clearing flag", (unsigned long)elapsed); AtomicWrite(&g_autoTeardownSuppressed, 0); return false; } extern "C" { __declspec(dllexport) EOS_EResult EOS_P2P_SendPacket(EOS_HP2P Handle, const EOS_P2P_SendPacketOptions *Options) { Log("[Proxy] >> EOS_P2P_SendPacket entry"); if (!Options) { Log("[Proxy] !! EOS_P2P_SendPacket rejected: Options is NULL"); return EOS_InvalidParameters; } InitNetwork(); struct sockaddr_in sendAddr; EnterCriticalSection(&g_socketCs); sendAddr = targetAddr; LeaveCriticalSection(&g_socketCs); EnterCriticalSection(&g_socketCs); int sent = sendto(udpSocket, (const char *)Options->Data, Options->DataLengthBytes, 0, (struct sockaddr *)&sendAddr, sizeof(sendAddr)); LeaveCriticalSection(&g_socketCs); if (InterlockedIncrement(&g_sendLogCount) <= 80 || sent == SOCKET_ERROR) { LogCallStack("EOS_P2P_SendPacket"); Log("[Proxy] SendPacket: requested=%u sent=%d channel=%u remote=%p", Options->DataLengthBytes, sent, Options->Channel, Options->RemoteUserId); } if (sent == SOCKET_ERROR) { Log("[Proxy] !! sendto failed: WSA %d", WSAGetLastError()); } else { g_lastGoodPacketTime = GetTickCount(); DumpPacket("SEND", Options->Data, (uint32_t)sent, &sendAddr, Options->Channel); } Log("[Proxy] << EOS_P2P_SendPacket exit (result=Success)"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_GetNextReceivedPacketSize( EOS_HP2P Handle, const EOS_P2P_GetNextReceivedPacketSizeOptions *Options, uint32_t *OutPacketSizeBytes) { if (AtomicRead(&g_resetInProgress)) return EOS_NotFound; InitNetwork(); if (!AtomicRead(&g_initialized)) return EOS_NotFound; if (!OutPacketSizeBytes) { Log("[Proxy] !! GetNextReceivedPacketSize rejected: OutPacketSizeBytes is NULL"); return EOS_InvalidParameters; } EnterCriticalSection(&g_socketCs); while (DiscardMitmHelloIfPresentLocked()) {} char tempBuf[65536]; struct sockaddr_in from; int fromLen = sizeof(from); int peeked = recvfrom(udpSocket, tempBuf, sizeof(tempBuf), MSG_PEEK, (struct sockaddr*)&from, &fromLen); LeaveCriticalSection(&g_socketCs); if (peeked > 0) { *OutPacketSizeBytes = (uint32_t)peeked; if (InterlockedIncrement(&g_sizeLogCount) <= 80) { int requestedChannel = (Options && Options->RequestedChannel) ? (int)*Options->RequestedChannel : -1; Log("[Proxy] GetNextReceivedPacketSize: %d bytes requestedChannel=%d", peeked, requestedChannel); } return EOS_Success; } return EOS_NotFound; } __declspec(dllexport) EOS_EResult EOS_P2P_ReceivePacket( EOS_HP2P Handle, const EOS_P2P_ReceivePacketOptions *Options, void *OutPeerId, void *OutSocketId, uint8_t *OutChannel, void *OutData, uint32_t *OutBytesWritten) { Log("[Proxy] >> EOS_P2P_ReceivePacket entry"); if (AtomicRead(&g_resetInProgress)) { Log("[Proxy] << EOS_P2P_ReceivePacket exit (reset in progress, NotFound)"); return EOS_NotFound; } InitNetwork(); if (!AtomicRead(&g_initialized)) { Log("[Proxy] << EOS_P2P_ReceivePacket exit (not initialized, NotFound)"); return EOS_NotFound; } if (!Options || !OutData || !OutBytesWritten) { Log("[Proxy] !! ReceivePacket rejected: bad args (Options=%p OutData=%p OutBytesWritten=%p)", (void*)Options, OutData, (void*)OutBytesWritten); return EOS_InvalidParameters; } struct sockaddr_in from; int fromLen = sizeof(from); EnterCriticalSection(&g_socketCs); while (DiscardMitmHelloIfPresentLocked()) {} int ret = recvfrom(udpSocket, (char *)OutData, Options->MaxDataSizeBytes, 0, (struct sockaddr *)&from, &fromLen); LeaveCriticalSection(&g_socketCs); if (ret > 0) { g_lastGoodPacketTime = GetTickCount(); // Only update host client address on first packet, without recursive locking if (AtomicRead(&g_isHost) && AtomicRead(&g_hostHasClientAddr) == 0) { HostOnFirstPacketReceivedNoLock(&from); } *OutBytesWritten = ret; uint8_t channel = Options->RequestedChannel ? *Options->RequestedChannel : 0; if (OutChannel) *OutChannel = channel; if (OutPeerId) *(EOS_ProductUserId *)OutPeerId = RemoteProductUserId(); if (OutSocketId) memcpy(OutSocketId, &g_socketId, sizeof(g_socketId)); DumpPacket("RECV", OutData, (uint32_t)ret, &from, channel); if (InterlockedIncrement(&g_receiveLogCount) <= 80) { LogCallStack("EOS_P2P_ReceivePacket"); Log("[Proxy] ReceivePacket: %d bytes from %s:%d peer=%p channel=%u", ret, inet_ntoa(from.sin_addr), ntohs(from.sin_port), RemoteProductUserId(), channel); } Log("[Proxy] << EOS_P2P_ReceivePacket exit (success, %d bytes)", ret); return EOS_Success; } Log("[Proxy] << EOS_P2P_ReceivePacket exit (NotFound)"); return EOS_NotFound; } __declspec(dllexport) EOS_EResult EOS_P2P_AcceptConnection(EOS_HP2P Handle, const void *Options) { Log("[Proxy] >> EOS_P2P_AcceptConnection called"); AtomicWrite(&g_peerConnected, 1); LogStateSnapshot("AcceptConnection"); FirePeerConnectionEstablished(RemoteProductUserId()); Log("[Proxy] << EOS_P2P_AcceptConnection exit"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_CloseConnection(EOS_HP2P Handle, const void *Options) { Log("[Proxy] EOS_P2P_CloseConnection called"); LogCallStack("EOS_P2P_CloseConnection"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_CloseConnections(EOS_HP2P Handle, const void *Options) { Log("[Proxy] EOS_P2P_CloseConnections called"); LogCallStack("EOS_P2P_CloseConnections"); // This is a bodge, this isnt a best practice at all, but it works bool userInitiated = StackContainsModule("USER32.dll"); bool socketHealthy = (udpSocket != INVALID_SOCKET) && (GetTickCount() - g_lastGoodPacketTime) < 4000; static LONG s_suppressedCount = 0; if (!userInitiated && socketHealthy && s_suppressedCount < 20) { LONG n = InterlockedIncrement(&s_suppressedCount); // Set the shared suppression flag so LeaveLobby and DestroyLobby // (which fire in the same tick) also skip their reset. AtomicWrite(&g_autoTeardownSuppressed, 1); g_autoTeardownSuppressTime = GetTickCount(); Log("[Proxy] EOS_P2P_CloseConnections SUPPRESSED (auto-teardown, socket healthy, " "suppressed=%ld, msSinceLastGoodPacket=%lu, suppression window started)", n, (unsigned long)(GetTickCount() - g_lastGoodPacketTime)); return EOS_Success; // tell the game it happened; don't actually reset anything } s_suppressedCount = 0; AtomicWrite(&g_autoTeardownSuppressed, 0); Log("[Proxy] EOS_P2P_CloseConnections executing real reset " "(userInitiated=%d socketHealthy=%d msSinceLastGoodPacket=%lu)", (int)userInitiated, (int)socketHealthy, (unsigned long)(GetTickCount() - g_lastGoodPacketTime)); ResetConnectionState(); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_QueryNATType(EOS_HP2P Handle, const void *Options, void *ClientData, void *CompletionDelegate) { Log("[Proxy] EOS_P2P_QueryNATType called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_GetNATType(EOS_HP2P Handle, const void *Options, int32_t *OutNATType) { Log("[Proxy] EOS_P2P_GetNATType called"); if (OutNATType) *OutNATType = 0; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_SetRelayControl(EOS_HP2P Handle, const void *Options) { Log("[Proxy] EOS_P2P_SetRelayControl called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_GetRelayControl(EOS_HP2P Handle, const void *Options, int32_t *OutRelayControl) { Log("[Proxy] EOS_P2P_GetRelayControl called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_SetPortRange(EOS_HP2P Handle, const void *Options) { Log("[Proxy] EOS_P2P_SetPortRange called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_GetPortRange(EOS_HP2P Handle, const void *Options, uint16_t *OutPort, uint16_t *OutNumAdditionalPortsToTry) { Log("[Proxy] EOS_P2P_GetPortRange called"); if (OutPort) *OutPort = 7777; if (OutNumAdditionalPortsToTry) *OutNumAdditionalPortsToTry = 0; return EOS_Success; } __declspec(dllexport) uint64_t EOS_P2P_AddNotifyPeerConnectionRequest( EOS_HP2P Handle, const void *Options, void *ClientData, EOS_P2P_OnIncomingConnectionRequestCallback NotificationFn) { static LONG g_addCount = 0; Log("[Proxy] EOS_P2P_AddNotifyPeerConnectionRequest called (count=%d)", (int)InterlockedIncrement(&g_addCount)); g_connectionRequestCb = NotificationFn; g_connectionRequestCbData = ClientData; Log("[Proxy] Stored connection request callback=%p data=%p", (void*)NotificationFn, ClientData); if (AtomicRead(&g_peerConnected) && NotificationFn) { Log("[Proxy] Peer already connected, immediately firing request"); FirePeerConnectionRequest(RemoteProductUserId()); } return g_notifIdCounter++; } __declspec(dllexport) void EOS_P2P_RemoveNotifyPeerConnectionRequest(EOS_HP2P Handle, uint64_t NotificationId) { Log("[Proxy] EOS_P2P_RemoveNotifyPeerConnectionRequest id=%llu", (unsigned long long)NotificationId); } __declspec(dllexport) uint64_t EOS_P2P_AddNotifyPeerConnectionEstablished( EOS_HP2P Handle, const void *Options, void *ClientData, EOS_P2P_OnPeerConnectionEstablishedCallback NotificationFn) { Log("[Proxy] EOS_P2P_AddNotifyPeerConnectionEstablished called cb=%p", (void*)NotificationFn); g_connectionEstablishedCb = NotificationFn; g_connectionEstablishedCbData = ClientData; if (AtomicRead(&g_peerConnected) && NotificationFn) { Log("[Proxy] Peer already connected, immediately firing established"); FirePeerConnectionEstablished(RemoteProductUserId()); } return g_notifIdCounter++; } __declspec(dllexport) void EOS_P2P_RemoveNotifyPeerConnectionEstablished(EOS_HP2P Handle, uint64_t NotificationId) { Log("[Proxy] EOS_P2P_RemoveNotifyPeerConnectionEstablished id=%llu", (unsigned long long)NotificationId); } __declspec(dllexport) uint64_t EOS_P2P_AddNotifyPeerConnectionInterrupted( EOS_HP2P Handle, const void *Options, void *ClientData, void *NotificationFn) { Log("[Proxy] EOS_P2P_AddNotifyPeerConnectionInterrupted called"); return g_notifIdCounter++; } __declspec(dllexport) void EOS_P2P_RemoveNotifyPeerConnectionInterrupted(EOS_HP2P Handle, uint64_t NotificationId) { Log("[Proxy] EOS_P2P_RemoveNotifyPeerConnectionInterrupted id=%llu", (unsigned long long)NotificationId); } __declspec(dllexport) uint64_t EOS_P2P_AddNotifyIncomingPacketQueueFull( EOS_HP2P Handle, const void *Options, void *ClientData, void *NotificationFn) { Log("[Proxy] EOS_P2P_AddNotifyIncomingPacketQueueFull called"); return g_notifIdCounter++; } __declspec(dllexport) void EOS_P2P_RemoveNotifyIncomingPacketQueueFull(EOS_HP2P Handle, uint64_t NotificationId) { Log("[Proxy] EOS_P2P_RemoveNotifyIncomingPacketQueueFull id=%llu", (unsigned long long)NotificationId); } __declspec(dllexport) EOS_EResult EOS_P2P_ClearPacketQueue(EOS_HP2P Handle, const void *Options) { Log("[Proxy] EOS_P2P_ClearPacketQueue called (ignored)"); LogCallStack("EOS_P2P_ClearPacketQueue"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_SetPacketQueueSize(EOS_HP2P Handle, const void *Options) { Log("[Proxy] EOS_P2P_SetPacketQueueSize called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_P2P_GetPacketQueueInfo(EOS_HP2P Handle, const void *Options, void *OutPacketQueueInfo) { Log("[Proxy] EOS_P2P_GetPacketQueueInfo called"); if (OutPacketQueueInfo) memset(OutPacketQueueInfo, 0, 32); return EOS_Success; } __declspec(dllexport) uint64_t EOS_P2P_AddNotifyPeerConnectionClosed(EOS_HP2P Handle, const void *Options, void *ClientData, void *NotificationFn) { Log("[Proxy] EOS_P2P_AddNotifyPeerConnectionClosed called"); return g_notifIdCounter++; } __declspec(dllexport) void EOS_P2P_RemoveNotifyPeerConnectionClosed(EOS_HP2P Handle, uint64_t NotificationId) { Log("[Proxy] EOS_P2P_RemoveNotifyPeerConnectionClosed id=%llu", (unsigned long long)NotificationId); } // Auth and Connect Mocking typedef void *EOS_HAuth; struct EOS_Auth_LoginOptions { int32_t ApiVersion; const void *Credentials; int32_t ScopeFlags; }; struct EOS_Auth_LoginCallbackInfo { int32_t ResultCode; void *ClientData; EOS_ProductUserId LocalUserId; void *PinGrantInfo; void *ContinuanceToken; void *AccountFeatureRestrictedInfo; EOS_ProductUserId SelectedAccountId; }; typedef void (*EOS_Auth_OnLoginCallback)(const EOS_Auth_LoginCallbackInfo *Data); typedef void *EOS_HConnect; struct EOS_Connect_LoginOptions { int32_t ApiVersion; const void *Credentials; const void *UserLoginInfo; }; struct EOS_Connect_LoginCallbackInfo { int32_t ResultCode; void *ClientData; EOS_ProductUserId LocalUserId; void *ContinuanceToken; }; typedef void (*EOS_Connect_OnLoginCallback)(const EOS_Connect_LoginCallbackInfo *Data); struct EOS_Connect_CreateUserOptions { int32_t ApiVersion; void *ContinuanceToken; }; struct EOS_Connect_CreateUserCallbackInfo { int32_t ResultCode; void *ClientData; EOS_ProductUserId LocalUserId; }; typedef void (*EOS_Connect_OnCreateUserCallback)(const EOS_Connect_CreateUserCallbackInfo *Data); __declspec(dllexport) void EOS_Auth_Login(EOS_HAuth Handle, const EOS_Auth_LoginOptions *Options, void *ClientData, EOS_Auth_OnLoginCallback CompletionDelegate) { Log("[Proxy] >> EOS_Auth_Login called"); if (CompletionDelegate) { EOS_Auth_LoginCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.LocalUserId = (EOS_ProductUserId)1; info.SelectedAccountId = (EOS_ProductUserId)1; Log("[Proxy] Invoking EOS_Auth_Login callback with Success"); CompletionDelegate(&info); } Log("[Proxy] << EOS_Auth_Login exit"); } struct EOS_Connect_LoginStatusChangedCallbackInfo { void* ClientData; EOS_ProductUserId LocalUserId; int32_t PreviousStatus; // EOS_ELoginStatus int32_t CurrentStatus; // EOS_ELoginStatus }; __declspec(dllexport) void EOS_Connect_Login(EOS_HConnect Handle, const EOS_Connect_LoginOptions *Options, void *ClientData, EOS_Connect_OnLoginCallback CompletionDelegate) { static LONG g_connectLoginCount = 0; Log("[Proxy] EOS_Connect_Login called (call #%d)", (int)InterlockedIncrement(&g_connectLoginCount)); EOS_ProductUserId localUser = LocalProductUserId(); if (CompletionDelegate) { EOS_Connect_LoginCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.LocalUserId = localUser; Log("[Proxy] Invoking EOS_Connect_Login callback localUser=%p", info.LocalUserId); CompletionDelegate(&info); } if (g_loginStatusChangedCb) { EOS_Connect_LoginStatusChangedCallbackInfo statusInfo = {0}; statusInfo.ClientData = g_loginStatusChangedCbData; statusInfo.LocalUserId = localUser; statusInfo.PreviousStatus = 0; // EOS_LS_NotLoggedIn statusInfo.CurrentStatus = 2; // EOS_LS_LoggedIn Log("[Proxy] Invoking LoginStatusChanged callback -> LoggedIn"); g_loginStatusChangedCb(&statusInfo); } } __declspec(dllexport) void EOS_Connect_CreateUser( EOS_HConnect Handle, const EOS_Connect_CreateUserOptions *Options, void *ClientData, EOS_Connect_OnCreateUserCallback CompletionDelegate) { Log("[Proxy] EOS_Connect_CreateUser called"); if (CompletionDelegate) { EOS_Connect_CreateUserCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.LocalUserId = LocalProductUserId(); CompletionDelegate(&info); } } __declspec(dllexport) uint64_t EOS_Connect_AddNotifyAuthExpiration(EOS_HConnect Handle, const void *Options, void *ClientData, void *NotificationFn) { Log("[Proxy] EOS_Connect_AddNotifyAuthExpiration called"); return g_notifIdCounter++; } // EOS_ELoginStatus: 0=NotLoggedIn, 1=UsingLocalProfile, 2=LoggedIn __declspec(dllexport) int32_t EOS_Connect_GetLoginStatus(EOS_HConnect Handle, EOS_ProductUserId LocalUserId) { Log("[Proxy] EOS_Connect_GetLoginStatus called -> LoggedIn (2)"); return 2; // EOS_LS_LoggedIn } __declspec(dllexport) uint64_t EOS_Connect_AddNotifyLoginStatusChanged(EOS_HConnect Handle, const void *Options, void *ClientData, EOS_Connect_OnLoginStatusChangedCallback NotificationFn) { Log("[Proxy] EOS_Connect_AddNotifyLoginStatusChanged called cb=%p", (void*)NotificationFn); g_loginStatusChangedCb = NotificationFn; g_loginStatusChangedCbData = ClientData; return g_notifIdCounter++; } __declspec(dllexport) void EOS_Connect_RemoveNotifyLoginStatusChanged(EOS_HConnect Handle, uint64_t InId) { Log("[Proxy] EOS_Connect_RemoveNotifyLoginStatusChanged id=%llu", (unsigned long long)InId); g_loginStatusChangedCb = nullptr; g_loginStatusChangedCbData = nullptr; } __declspec(dllexport) int32_t EOS_Auth_GetLoginStatus(EOS_HAuth Handle, EOS_ProductUserId LocalUserId) { Log("[Proxy] EOS_Auth_GetLoginStatus called -> LoggedIn (2)"); return 2; } __declspec(dllexport) EOS_ProductUserId EOS_EpicAccountId_FromString(const char *AccountIdString) { Log("[Proxy] EOS_EpicAccountId_FromString(%s)", AccountIdString ? AccountIdString : "NULL"); return (EOS_ProductUserId)1; } __declspec(dllexport) EOS_ProductUserId EOS_ProductUserId_FromString(const char *AccountIdString) { Log("[Proxy] EOS_ProductUserId_FromString(%s)", AccountIdString ? AccountIdString : "NULL"); if (AccountIdString && strstr(AccountIdString, "HOST")) return HostProductUserId(); if (AccountIdString && strstr(AccountIdString, "CLIENT")) return ClientProductUserId(); return LocalProductUserId(); } __declspec(dllexport) int32_t EOS_EpicAccountId_IsValid(EOS_ProductUserId AccountId) { return 1; } __declspec(dllexport) int32_t EOS_ProductUserId_IsValid(EOS_ProductUserId AccountId) { int valid = (AccountId == HostProductUserId() || AccountId == ClientProductUserId()); Log("[Proxy] EOS_ProductUserId_IsValid(%p) -> %d", AccountId, valid); return valid; } __declspec(dllexport) EOS_EResult EOS_ProductUserId_ToString(EOS_ProductUserId AccountId, char *OutBuffer, int32_t *InOutBufferLength) { const char *value = nullptr; if (AccountId == HostProductUserId()) value = "DIRECTIP_HOST_PUID"; else if (AccountId == ClientProductUserId()) value = "DIRECTIP_CLIENT_PUID"; else { Log("[Proxy] !! EOS_ProductUserId_ToString unknown AccountId=%p", AccountId); return EOS_InvalidParameters; } int32_t needed = (int32_t)strlen(value) + 1; if (!InOutBufferLength) return EOS_InvalidParameters; if (!OutBuffer || *InOutBufferLength < needed) { *InOutBufferLength = needed; return EOS_InvalidParameters; } memcpy(OutBuffer, value, needed); *InOutBufferLength = needed; Log("[Proxy] EOS_ProductUserId_ToString(%p) -> %s", AccountId, value); return EOS_Success; } struct EOS_Connect_CopyIdTokenOptions { int32_t ApiVersion; EOS_ProductUserId LocalUserId; }; struct EOS_Connect_IdToken { int32_t ApiVersion; EOS_ProductUserId ProductUserId; const char* JsonWebToken; }; __declspec(dllexport) EOS_EResult EOS_Connect_CopyIdToken( EOS_HConnect Handle, const EOS_Connect_CopyIdTokenOptions* Options, EOS_Connect_IdToken** OutIdToken) { Log("[Proxy] EOS_Connect_CopyIdToken called"); if (!Options || !OutIdToken) return EOS_InvalidParameters; EOS_Connect_IdToken* token = (EOS_Connect_IdToken*)calloc(1, sizeof(EOS_Connect_IdToken)); if (!token) return EOS_InvalidParameters; token->ApiVersion = 1; token->ProductUserId = Options->LocalUserId ? Options->LocalUserId : LocalProductUserId(); token->JsonWebToken = "eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwicHJvZHVjdFVzZXJJZCI6IkRJUkVDVElQX0hPU1RfUFVJRCJ9."; *OutIdToken = token; Log("[Proxy] EOS_Connect_CopyIdToken -> success, token=%p", (void*)token); return EOS_Success; } __declspec(dllexport) void EOS_Connect_IdToken_Release(EOS_Connect_IdToken* IdToken) { Log("[Proxy] EOS_Connect_IdToken_Release called"); if (IdToken) { free(IdToken); } } struct EOS_Connect_VerifyIdTokenOptions { int32_t ApiVersion; const EOS_Connect_IdToken* IdToken; }; struct EOS_Connect_VerifyIdTokenCallbackInfo { int32_t ResultCode; void* ClientData; EOS_ProductUserId ProductUserId; }; typedef void (*EOS_Connect_OnVerifyIdTokenCallback)(const EOS_Connect_VerifyIdTokenCallbackInfo* Data); __declspec(dllexport) void EOS_Connect_VerifyIdToken( EOS_HConnect Handle, const EOS_Connect_VerifyIdTokenOptions* Options, void* ClientData, EOS_Connect_OnVerifyIdTokenCallback CompletionDelegate) { Log("[Proxy] EOS_Connect_VerifyIdToken called"); if (CompletionDelegate) { EOS_Connect_VerifyIdTokenCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.ProductUserId = (Options && Options->IdToken) ? Options->IdToken->ProductUserId : LocalProductUserId(); Log("[Proxy] Invoking EOS_Connect_VerifyIdToken callback -> Success"); CompletionDelegate(&info); } } // Lobby Mocking typedef void *EOS_HLobby; typedef void *EOS_HLobbySearch; typedef void *EOS_HLobbyDetails; struct EOS_Lobby_CreateLobbyCallbackInfo { int32_t ResultCode; void *ClientData; const char *LobbyId; }; typedef void (*EOS_Lobby_OnCreateLobbyCallback)(const EOS_Lobby_CreateLobbyCallbackInfo *Data); struct EOS_Lobby_JoinLobbyCallbackInfo { int32_t ResultCode; void *ClientData; const char *LobbyId; }; typedef void (*EOS_Lobby_OnJoinLobbyCallback)(const EOS_Lobby_JoinLobbyCallbackInfo *Data); struct EOS_Lobby_JoinLobbyByIdOptions { int32_t ApiVersion; const char* LobbyId; EOS_ProductUserId LocalUserId; }; struct EOS_LobbySearch_FindCallbackInfo { int32_t ResultCode; void *ClientData; }; typedef void (*EOS_LobbySearch_OnFindCallback)(const EOS_LobbySearch_FindCallbackInfo *Data); struct EOS_LobbySearch_CopySearchResultByIndexOptions { int32_t ApiVersion; uint32_t LobbyIndex; }; struct EOS_LobbyDetails_Info { int32_t ApiVersion; const char *LobbyId; EOS_ProductUserId LobbyOwnerUserId; uint32_t PermissionLevel; uint32_t AvailableSlots; uint32_t MaxMembers; int32_t bAllowInvites; const char *BucketId; int32_t bAllowHostMigration; int32_t bRTCRoomEnabled; int32_t bAllowJoinById; int32_t bRejoinAfterKickRequiresInvite; }; __declspec(dllexport) void EOS_Lobby_CreateLobby(EOS_HLobby Handle, const void *Options, void *ClientData, EOS_Lobby_OnCreateLobbyCallback CompletionDelegate) { Log("[Proxy] >> EOS_Lobby_CreateLobby called"); LogCallStack("EOS_Lobby_CreateLobby"); InitNetwork(); if (!AtomicRead(&g_isHost) && AtomicRead(&g_clientSawSearchResult)) { Log("[Proxy] Client called CreateLobby after seeing search result; forcing DirectIP join behavior"); AtomicWrite(&g_lobbyHasRemoteMember, 1); } EnterCriticalSection(&g_socketCs); if (!g_peerWatchThread) { ResetEvent(g_stopEvent); LONG gen = InterlockedIncrement(&g_watchThreadGeneration); g_peerWatchThread = CreateThread(NULL, 0, PeerWatchThread, (LPVOID)(intptr_t)gen, 0, NULL); Log("[Proxy] Started PeerWatchThread from CreateLobby (generation=%ld)", (long)gen); } else { Log("[Proxy] CreateLobby: PeerWatchThread already running, not spawning a second one"); } LeaveCriticalSection(&g_socketCs); if (CompletionDelegate) { EOS_Lobby_CreateLobbyCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.LobbyId = "DirectIP_Lobby"; Log("[Proxy] Invoking CreateLobby callback"); CompletionDelegate(&info); } LogStateSnapshot("CreateLobby"); Log("[Proxy] << EOS_Lobby_CreateLobby exit"); } __declspec(dllexport) void EOS_Lobby_JoinLobby(EOS_HLobby Handle, const void *Options, void *ClientData, EOS_Lobby_OnJoinLobbyCallback CompletionDelegate) { Log("[Proxy] >> EOS_Lobby_JoinLobby called"); LogCallStack("EOS_Lobby_JoinLobby"); InitNetwork(); if (!AtomicRead(&g_isHost)) AtomicWrite(&g_lobbyHasRemoteMember, 1); EnterCriticalSection(&g_socketCs); if (!g_peerWatchThread) { ResetEvent(g_stopEvent); LONG gen = InterlockedIncrement(&g_watchThreadGeneration); g_peerWatchThread = CreateThread(NULL, 0, PeerWatchThread, (LPVOID)(intptr_t)gen, 0, NULL); Log("[Proxy] Started PeerWatchThread from JoinLobby (generation=%ld)", (long)gen); } else { Log("[Proxy] JoinLobby: PeerWatchThread already running, not spawning a second one"); } LeaveCriticalSection(&g_socketCs); if (CompletionDelegate) { EOS_Lobby_JoinLobbyCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.LobbyId = "DirectIP_Lobby"; Log("[Proxy] Invoking JoinLobby callback"); CompletionDelegate(&info); } LogStateSnapshot("JoinLobby"); Log("[Proxy] << EOS_Lobby_JoinLobby exit"); } __declspec(dllexport) void EOS_Lobby_JoinLobbyById(EOS_HLobby Handle, const EOS_Lobby_JoinLobbyByIdOptions* Options, void* ClientData, EOS_Lobby_OnJoinLobbyCallback CompletionDelegate) { Log("[Proxy] >> EOS_Lobby_JoinLobbyById called id=%s", Options ? Options->LobbyId : "NULL"); LogCallStack("EOS_Lobby_JoinLobbyById"); InitNetwork(); if (!AtomicRead(&g_isHost)) AtomicWrite(&g_lobbyHasRemoteMember, 1); EnterCriticalSection(&g_socketCs); if (!g_peerWatchThread) { ResetEvent(g_stopEvent); LONG gen = InterlockedIncrement(&g_watchThreadGeneration); g_peerWatchThread = CreateThread(NULL, 0, PeerWatchThread, (LPVOID)(intptr_t)gen, 0, NULL); Log("[Proxy] Started PeerWatchThread from JoinLobbyById (generation=%ld)", (long)gen); } else { Log("[Proxy] JoinLobbyById: PeerWatchThread already running, not spawning a second one"); } LeaveCriticalSection(&g_socketCs); if (CompletionDelegate) { EOS_Lobby_JoinLobbyCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; info.LobbyId = "DirectIP_Lobby"; Log("[Proxy] Invoking JoinLobbyById callback"); CompletionDelegate(&info); } LogStateSnapshot("JoinLobbyById"); Log("[Proxy] << EOS_Lobby_JoinLobbyById exit"); } __declspec(dllexport) EOS_EResult EOS_Lobby_CreateLobbySearch(EOS_HLobby Handle, const void *Options, EOS_HLobbySearch *OutLobbySearchHandle) { Log("[Proxy] EOS_Lobby_CreateLobbySearch called"); if (OutLobbySearchHandle) *OutLobbySearchHandle = (EOS_HLobbySearch)3; return EOS_Success; } __declspec(dllexport) void EOS_LobbySearch_Find(EOS_HLobbySearch Handle, const void *Options, void *ClientData, EOS_LobbySearch_OnFindCallback CompletionDelegate) { Log("[Proxy] >> EOS_LobbySearch_Find called"); if (CompletionDelegate) { EOS_LobbySearch_FindCallbackInfo info = {0}; info.ResultCode = EOS_Success; info.ClientData = ClientData; Log("[Proxy] Invoking LobbySearch_Find callback"); CompletionDelegate(&info); } Log("[Proxy] << EOS_LobbySearch_Find exit"); } __declspec(dllexport) uint32_t EOS_LobbySearch_GetSearchResultCount( EOS_HLobbySearch Handle, const void *Options) { InitNetwork(); uint32_t count = AtomicRead(&g_isHost) ? 0 : 1; Log("[Proxy] EOS_LobbySearch_GetSearchResultCount -> %u", count); return count; } __declspec(dllexport) EOS_EResult EOS_LobbySearch_CopySearchResultByIndex( EOS_HLobbySearch Handle, const EOS_LobbySearch_CopySearchResultByIndexOptions *Options, EOS_HLobbyDetails *OutLobbyDetailsHandle) { Log("[Proxy] EOS_LobbySearch_CopySearchResultByIndex called"); AtomicWrite(&g_clientSawSearchResult, 1); LogStateSnapshot("CopySearchResultByIndex"); if (OutLobbyDetailsHandle) *OutLobbyDetailsHandle = (EOS_HLobbyDetails)4; return EOS_Success; } static EOS_LobbyDetails_Info dummyLobbyInfo = { 2, "DirectIP_Lobby", (EOS_ProductUserId)1, 0, 1, 4, 1, "DirectIP_Bucket", 0, 0, 1, 0 }; __declspec(dllexport) EOS_EResult EOS_LobbyDetails_CopyInfo(EOS_HLobbyDetails Handle, const void *Options, EOS_LobbyDetails_Info **OutLobbyDetailsInfo) { uint32_t currentMembers = (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected)) ? 2 : 1; dummyLobbyInfo.AvailableSlots = g_maxMembers - currentMembers; dummyLobbyInfo.MaxMembers = g_maxMembers; if (InterlockedIncrement(&g_copyInfoLogCount) <= 20) { Log("[Proxy] EOS_LobbyDetails_CopyInfo owner=%p slots=%u/%u bucket=%s", dummyLobbyInfo.LobbyOwnerUserId, dummyLobbyInfo.AvailableSlots, dummyLobbyInfo.MaxMembers, dummyLobbyInfo.BucketId); } if (OutLobbyDetailsInfo) *OutLobbyDetailsInfo = &dummyLobbyInfo; return EOS_Success; } __declspec(dllexport) EOS_ProductUserId EOS_LobbyDetails_GetLobbyOwner(EOS_HLobbyDetails Handle, const void *Options) { if (InterlockedIncrement(&g_ownerLogCount) <= 80) Log("[Proxy] EOS_LobbyDetails_GetLobbyOwner called"); return HostProductUserId(); } __declspec(dllexport) void EOS_LobbyDetails_Release(void* LobbyDetailsHandle) { (void)LobbyDetailsHandle; } __declspec(dllexport) void EOS_LobbySearch_Release(EOS_HLobbySearch LobbySearchHandle) { Log("[Proxy] EOS_LobbySearch_Release called"); } __declspec(dllexport) EOS_EResult EOS_LobbySearch_RemoveParameter(EOS_HLobbySearch Handle, const void *Options) { Log("[Proxy] EOS_LobbySearch_RemoveParameter called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbySearch_SetLobbyId(EOS_HLobbySearch Handle, const void *Options) { Log("[Proxy] EOS_LobbySearch_SetLobbyId called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbySearch_SetMaxResults(EOS_HLobbySearch Handle, const void *Options) { Log("[Proxy] EOS_LobbySearch_SetMaxResults called"); return EOS_Success; } struct EOS_Lobby_AttributeData { int32_t ApiVersion; const char* Key; union { int64_t AsInt64; double AsDouble; int32_t AsBool; const char* AsUtf8; } Value; int32_t ValueType; }; struct EOS_LobbySearch_SetParameterOptions { int32_t ApiVersion; const EOS_Lobby_AttributeData* Parameter; int32_t ComparisonOp; }; __declspec(dllexport) EOS_EResult EOS_LobbySearch_SetParameter(EOS_HLobbySearch Handle, const EOS_LobbySearch_SetParameterOptions *Options) { if (Options && Options->Parameter && Options->Parameter->Key) { if (Options->Parameter->ValueType == 3) { Log("[Proxy] EOS_LobbySearch_SetParameter: %s = %s", Options->Parameter->Key, Options->Parameter->Value.AsUtf8); if (Options->Parameter->Value.AsUtf8) { if (strcmp(Options->Parameter->Key, "AppVersion") == 0) { strncpy(g_searchAppVersion, Options->Parameter->Value.AsUtf8, sizeof(g_searchAppVersion) - 1); g_searchAppVersion[sizeof(g_searchAppVersion) - 1] = '\0'; } else if (strcmp(Options->Parameter->Key, "OnlineRule") == 0) { strncpy(g_searchOnlineRule, Options->Parameter->Value.AsUtf8, sizeof(g_searchOnlineRule) - 1); g_searchOnlineRule[sizeof(g_searchOnlineRule) - 1] = '\0'; } } } else { Log("[Proxy] EOS_LobbySearch_SetParameter: %s (Type: %d)", Options->Parameter->Key, Options->Parameter->ValueType); } } else { Log("[Proxy] EOS_LobbySearch_SetParameter called (empty)"); } return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbySearch_SetTargetUserId(EOS_HLobbySearch Handle, const void *Options) { Log("[Proxy] EOS_LobbySearch_SetTargetUserId called"); return EOS_Success; } typedef void *EOS_HLobbyModification; struct EOS_LobbyModification_AddAttributeOptions { int32_t ApiVersion; const EOS_Lobby_AttributeData* Attribute; int32_t Visibility; }; __declspec(dllexport) EOS_EResult EOS_LobbyModification_AddAttribute( EOS_HLobbyModification Handle, const EOS_LobbyModification_AddAttributeOptions *Options) { if (Options && Options->Attribute && Options->Attribute->Key) { if (Options->Attribute->ValueType == 3) { Log("[Proxy] EOS_LobbyModification_AddAttribute: %s = %s", Options->Attribute->Key, Options->Attribute->Value.AsUtf8); if (Options->Attribute->Value.AsUtf8) { if (strcmp(Options->Attribute->Key, "AppVersion") == 0) { strncpy(g_lobbyAppVersion, Options->Attribute->Value.AsUtf8, sizeof(g_lobbyAppVersion) - 1); g_lobbyAppVersion[sizeof(g_lobbyAppVersion) - 1] = '\0'; } else if (strcmp(Options->Attribute->Key, "OnlineRule") == 0) { strncpy(g_lobbyOnlineRule, Options->Attribute->Value.AsUtf8, sizeof(g_lobbyOnlineRule) - 1); g_lobbyOnlineRule[sizeof(g_lobbyOnlineRule) - 1] = '\0'; } else if (strcmp(Options->Attribute->Key, "Platform") == 0) { strncpy(g_lobbyPlatform, Options->Attribute->Value.AsUtf8, sizeof(g_lobbyPlatform) - 1); g_lobbyPlatform[sizeof(g_lobbyPlatform) - 1] = '\0'; } else if (strcmp(Options->Attribute->Key, "EosProductID") == 0) { strncpy(g_lobbyEosProductId, Options->Attribute->Value.AsUtf8, sizeof(g_lobbyEosProductId) - 1); g_lobbyEosProductId[sizeof(g_lobbyEosProductId) - 1] = '\0'; } else if (strcmp(Options->Attribute->Key, "IsCrossPlatform") == 0) { g_lobbyIsCrossPlatform = _stricmp(Options->Attribute->Value.AsUtf8, "true") == 0 || strcmp(Options->Attribute->Value.AsUtf8, "1") == 0; } } } else { if (Options->Attribute->ValueType == 1) { Log("[Proxy] EOS_LobbyModification_AddAttribute: %s = %lld (int64)", Options->Attribute->Key, (long long)Options->Attribute->Value.AsInt64); if (strcmp(Options->Attribute->Key, "WinRate") == 0) g_lobbyWinRate = Options->Attribute->Value.AsInt64; } else if (Options->Attribute->ValueType == 0) { Log("[Proxy] EOS_LobbyModification_AddAttribute: %s = %d (bool)", Options->Attribute->Key, Options->Attribute->Value.AsBool); } else { Log("[Proxy] EOS_LobbyModification_AddAttribute: %s (Type: %d)", Options->Attribute->Key, Options->Attribute->ValueType); } } } else { Log("[Proxy] EOS_LobbyModification_AddAttribute called (empty)"); } return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_AddMemberAttribute( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_AddMemberAttribute called"); return EOS_Success; } __declspec(dllexport) void EOS_LobbyModification_Release(EOS_HLobbyModification Handle) { Log("[Proxy] EOS_LobbyModification_Release called"); } __declspec(dllexport) EOS_EResult EOS_LobbyModification_RemoveAttribute( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_RemoveAttribute called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_RemoveMemberAttribute( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_RemoveMemberAttribute called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_SetAllowedPlatformIds( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_SetAllowedPlatformIds called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_SetBucketId( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_SetBucketId called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_SetInvitesAllowed( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_SetInvitesAllowed called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_SetMaxMembers( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_SetMaxMembers called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyModification_SetPermissionLevel( EOS_HLobbyModification Handle, const void *Options) { Log("[Proxy] EOS_LobbyModification_SetPermissionLevel called"); return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Lobby_UpdateLobbyModification( EOS_HLobby Handle, const void *Options, EOS_HLobbyModification *OutLobbyModificationHandle) { Log("[Proxy] EOS_Lobby_UpdateLobbyModification called"); if (OutLobbyModificationHandle) *OutLobbyModificationHandle = (EOS_HLobbyModification)5; return EOS_Success; } __declspec(dllexport) uint64_t EOS_Lobby_AddNotifyLobbyUpdateReceived(EOS_HLobby Handle, const void *Options, void *ClientData, void *NotificationFn) { Log("[Proxy] EOS_Lobby_AddNotifyLobbyUpdateReceived stored cb=%p", NotificationFn); g_lobbyUpdateCb = (EOS_Lobby_OnLobbyUpdateReceivedCallback)NotificationFn; g_lobbyUpdateCbData = ClientData; return g_notifIdCounter++; } __declspec(dllexport) void EOS_Lobby_RemoveNotifyLobbyUpdateReceived(EOS_HLobby Handle, uint64_t InId) { Log("[Proxy] EOS_Lobby_RemoveNotifyLobbyUpdateReceived id=%llu", (unsigned long long)InId); } struct EOS_Lobby_Attribute { int32_t ApiVersion; EOS_Lobby_AttributeData* Data; int32_t Visibility; }; static EOS_Lobby_AttributeData attrData1 = { 1, "AppVersion", {.AsUtf8 = "1.2.0"}, 3 }; static EOS_Lobby_Attribute attr1 = { 1, &attrData1, 0 }; static EOS_Lobby_AttributeData attrData2 = { 1, "OnlineRule", {.AsUtf8 = "Koikoi"}, 3 }; static EOS_Lobby_Attribute attr2 = { 1, &attrData2, 0 }; static EOS_Lobby_AttributeData attrData3 = { 1, "IsCrossPlatform", {.AsUtf8 = "true"}, 3 }; static EOS_Lobby_Attribute attr3 = { 1, &attrData3, 0 }; static EOS_Lobby_AttributeData attrData4 = { 1, "Platform", {.AsUtf8 = "Steam"}, 3 }; static EOS_Lobby_Attribute attr4 = { 1, &attrData4, 0 }; static EOS_Lobby_AttributeData attrData5 = { 1, "WinRate", {.AsInt64 = 0}, 1 }; static EOS_Lobby_Attribute attr5 = { 1, &attrData5, 0 }; static EOS_Lobby_AttributeData attrData6 = { 1, "EosProductID", {.AsUtf8 = ""}, 3 }; static EOS_Lobby_Attribute attr6 = { 1, &attrData6, 0 }; static EOS_Lobby_Attribute* LobbyAttributeByIndex(uint32_t index) { attrData1.Value.AsUtf8 = g_searchAppVersion[0] ? g_searchAppVersion : g_lobbyAppVersion; attrData2.Value.AsUtf8 = g_searchOnlineRule[0] ? g_searchOnlineRule : g_lobbyOnlineRule; attrData3.Value.AsUtf8 = g_lobbyIsCrossPlatform ? "true" : "false"; attrData4.Value.AsUtf8 = g_lobbyPlatform; attrData5.Value.AsInt64 = g_lobbyWinRate; attrData6.Value.AsUtf8 = g_lobbyEosProductId[0] ? g_lobbyEosProductId : "DIRECTIP_HOST_PUID"; switch (index) { case 0: return &attr1; case 1: return &attr2; case 2: return &attr3; case 3: return &attr4; case 4: return &attr5; case 5: return &attr6; default: return nullptr; } } static EOS_Lobby_Attribute* LobbyAttributeByKey(const char* key) { if (!key) return nullptr; for (uint32_t i = 0; i < 6; ++i) { EOS_Lobby_Attribute* attr = LobbyAttributeByIndex(i); if (attr && attr->Data && strcmp(attr->Data->Key, key) == 0) return attr; } return nullptr; } __declspec(dllexport) uint32_t EOS_LobbyDetails_GetAttributeCount( EOS_HLobbyDetails Handle, const void *Options) { Log("[Proxy] EOS_LobbyDetails_GetAttributeCount called -> 6"); return 6; } __declspec(dllexport) EOS_EResult EOS_LobbyDetails_CopyAttributeByIndex(EOS_HLobbyDetails Handle, const void* Options, EOS_Lobby_Attribute** OutAttribute) { if (!Options || !OutAttribute) return EOS_InvalidParameters; uint32_t index = ((const uint32_t*)Options)[1]; EOS_Lobby_Attribute* attr = LobbyAttributeByIndex(index); if (!attr) return EOS_NotFound; if (attr->Data->ValueType == 3) { Log("[Proxy] CopyAttributeByIndex %u -> %s = %s", index, attr->Data->Key, attr->Data->Value.AsUtf8); } else if (attr->Data->ValueType == 1) { Log("[Proxy] CopyAttributeByIndex %u -> %s = %lld", index, attr->Data->Key, (long long)attr->Data->Value.AsInt64); } else { Log("[Proxy] CopyAttributeByIndex %u -> %s type=%d", index, attr->Data->Key, attr->Data->ValueType); } *OutAttribute = attr; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_LobbyDetails_CopyAttributeByKey(EOS_HLobbyDetails Handle, const void* Options, EOS_Lobby_Attribute** OutAttribute) { if (!Options || !OutAttribute) return EOS_InvalidParameters; const char* key = ((const char* const*)Options)[1]; EOS_Lobby_Attribute* attr = LobbyAttributeByKey(key); if (!attr) { Log("[Proxy] CopyAttributeByKey(%s) -> NotFound", key ? key : "NULL"); return EOS_NotFound; } if (attr->Data->ValueType == 3) { Log("[Proxy] CopyAttributeByKey %s = %s", key, attr->Data->Value.AsUtf8); } else if (attr->Data->ValueType == 1) { Log("[Proxy] CopyAttributeByKey %s = %lld", key, (long long)attr->Data->Value.AsInt64); } else { Log("[Proxy] CopyAttributeByKey %s type=%d", key, attr->Data->ValueType); } *OutAttribute = attr; return EOS_Success; } __declspec(dllexport) void EOS_Lobby_Attribute_Release(EOS_Lobby_Attribute* Attribute) { (void)Attribute; } struct EOS_LobbyDetails_CopyMemberInfoOptions { int32_t ApiVersion; EOS_ProductUserId TargetUserId; }; struct EOS_LobbyDetails_MemberInfo { int32_t ApiVersion; EOS_ProductUserId ProductUserId; }; struct EOS_LobbyDetails_GetMemberByIndexOptions { int32_t ApiVersion; uint32_t MemberIndex; }; __declspec(dllexport) EOS_EResult EOS_LobbyDetails_CopyMemberAttributeByIndex(EOS_HLobbyDetails Handle, const void* Options, void** OutAttribute) { Log("[Proxy] EOS_LobbyDetails_CopyMemberAttributeByIndex called"); if(OutAttribute) *OutAttribute = nullptr; return EOS_NotFound; } __declspec(dllexport) EOS_EResult EOS_LobbyDetails_CopyMemberAttributeByKey(EOS_HLobbyDetails Handle, const void* Options, void** OutAttribute) { Log("[Proxy] EOS_LobbyDetails_CopyMemberAttributeByKey called"); if(OutAttribute) *OutAttribute = nullptr; return EOS_NotFound; } __declspec(dllexport) EOS_EResult EOS_LobbyDetails_CopyMemberInfo( EOS_HLobbyDetails Handle, const void* Options, void** OutMemberInfo) { static EOS_LobbyDetails_MemberInfo hostInfo = {1, (EOS_ProductUserId)1}; static EOS_LobbyDetails_MemberInfo clientInfo = {1, (EOS_ProductUserId)2}; EOS_ProductUserId targetUserId = LocalProductUserId(); if (Options) { const EOS_LobbyDetails_CopyMemberInfoOptions* opts = (const EOS_LobbyDetails_CopyMemberInfoOptions*)Options; if (opts->TargetUserId) targetUserId = opts->TargetUserId; } EOS_LobbyDetails_MemberInfo* info = nullptr; if (targetUserId == HostProductUserId()) info = &hostInfo; else if (targetUserId == ClientProductUserId() && (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected))) info = &clientInfo; if (InterlockedIncrement(&g_memberInfoLogCount) <= 60) Log("[Proxy] CopyMemberInfo target=%p result=%s", targetUserId, info ? "success" : "notfound"); if (!info) { if (OutMemberInfo) *OutMemberInfo = nullptr; return EOS_NotFound; } if (OutMemberInfo) *OutMemberInfo = info; return EOS_Success; } __declspec(dllexport) uint32_t EOS_LobbyDetails_GetMemberAttributeCount( EOS_HLobbyDetails Handle, const void *Options) { return 0; } __declspec(dllexport) EOS_ProductUserId EOS_LobbyDetails_GetMemberByIndex( EOS_HLobbyDetails Handle, const void *Options) { uint32_t memberIndex = 0; if (Options) { const EOS_LobbyDetails_GetMemberByIndexOptions* opts = (const EOS_LobbyDetails_GetMemberByIndexOptions*)Options; memberIndex = opts->MemberIndex; } EOS_ProductUserId member = nullptr; if (memberIndex == 0) member = HostProductUserId(); else if (memberIndex == 1 && (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected))) member = ClientProductUserId(); if (InterlockedIncrement(&g_memberByIndexLogCount) <= 80) Log("[Proxy] GetMemberByIndex index=%u -> %p", memberIndex, member); return member; } __declspec(dllexport) uint32_t EOS_LobbyDetails_GetMemberCount(EOS_HLobbyDetails Handle, const void *Options) { uint32_t count = (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected)) ? 2 : 1; if (InterlockedIncrement(&g_memberCountLogCount) <= 80) Log("[Proxy] GetMemberCount -> %u", count); return count; } __declspec(dllexport) void EOS_LobbyDetails_Info_Release(void *LobbyDetailsInfo) { (void)LobbyDetailsInfo; } __declspec(dllexport) void EOS_LobbyDetails_MemberInfo_Release(void *LobbyDetailsMemberInfo) { (void)LobbyDetailsMemberInfo; } __declspec(dllexport) EOS_EResult EOS_Lobby_CopyLobbyDetailsHandle(EOS_HLobby Handle, const void *Options, EOS_HLobbyDetails *OutLobbyDetailsHandle) { Log("[Proxy] EOS_Lobby_CopyLobbyDetailsHandle called"); if (OutLobbyDetailsHandle) *OutLobbyDetailsHandle = (EOS_HLobbyDetails)4; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Lobby_CopyLobbyDetailsHandleByInviteId( EOS_HLobby Handle, const void *Options, EOS_HLobbyDetails *OutLobbyDetailsHandle) { Log("[Proxy] EOS_Lobby_CopyLobbyDetailsHandleByInviteId called"); if (OutLobbyDetailsHandle) *OutLobbyDetailsHandle = (EOS_HLobbyDetails)4; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Lobby_CopyLobbyDetailsHandleByUiEventId( EOS_HLobby Handle, const void *Options, EOS_HLobbyDetails *OutLobbyDetailsHandle) { Log("[Proxy] EOS_Lobby_CopyLobbyDetailsHandleByUiEventId called"); if (OutLobbyDetailsHandle) *OutLobbyDetailsHandle = (EOS_HLobbyDetails)4; return EOS_Success; } struct EOS_Lobby_UpdateLobbyCallbackInfo { int32_t ResultCode; void *ClientData; const char *LobbyId; }; typedef void (*EOS_Lobby_OnUpdateLobbyCallback)(const EOS_Lobby_UpdateLobbyCallbackInfo *Data); __declspec(dllexport) void EOS_Lobby_UpdateLobby(EOS_HLobby Handle, const void *Options, void *ClientData, EOS_Lobby_OnUpdateLobbyCallback CompletionDelegate) { Log("[Proxy] >> EOS_Lobby_UpdateLobby called"); if (CompletionDelegate) { EOS_Lobby_UpdateLobbyCallbackInfo info = {EOS_Success, ClientData, "DirectIP_Lobby"}; CompletionDelegate(&info); } if (g_lobbyUpdateCb) { EOS_Lobby_LobbyUpdateReceivedCallbackInfo notif = {0}; notif.ClientData = g_lobbyUpdateCbData; notif.LobbyId = "DirectIP_Lobby"; g_lobbyUpdateCb(¬if); } if (g_memberUpdateCb) { EOS_Lobby_LobbyMemberUpdateReceivedCallbackInfo notif = {0}; notif.ClientData = g_memberUpdateCbData; notif.LobbyId = "DirectIP_Lobby"; notif.TargetUserId = RemoteProductUserId(); g_memberUpdateCb(¬if); } Log("[Proxy] << EOS_Lobby_UpdateLobby exit"); } struct EOS_Lobby_DestroyLobbyCallbackInfo { int32_t ResultCode; void *ClientData; const char *LobbyId; }; typedef void (*EOS_Lobby_OnDestroyLobbyCallback)(const EOS_Lobby_DestroyLobbyCallbackInfo *Data); __declspec(dllexport) void EOS_Lobby_DestroyLobby(EOS_HLobby Handle, const void *Options, void *ClientData, EOS_Lobby_OnDestroyLobbyCallback CompletionDelegate) { Log("[Proxy] EOS_Lobby_DestroyLobby called"); LogCallStack("EOS_Lobby_DestroyLobby"); if (!ShouldSuppressAutoTeardown()) { ResetConnectionState(); } if (CompletionDelegate) { EOS_Lobby_DestroyLobbyCallbackInfo info = {EOS_Success, ClientData, "DirectIP_Lobby"}; CompletionDelegate(&info); } } struct EOS_Lobby_LeaveLobbyCallbackInfo { int32_t ResultCode; void *ClientData; const char *LobbyId; }; typedef void (*EOS_Lobby_OnLeaveLobbyCallback)(const EOS_Lobby_LeaveLobbyCallbackInfo *Data); __declspec(dllexport) void EOS_Lobby_LeaveLobby(EOS_HLobby Handle, const void *Options, void *ClientData, EOS_Lobby_OnLeaveLobbyCallback CompletionDelegate) { Log("[Proxy] EOS_Lobby_LeaveLobby called"); LogCallStack("EOS_Lobby_LeaveLobby"); if (!ShouldSuppressAutoTeardown()) { ResetConnectionState(); } if (CompletionDelegate) { EOS_Lobby_LeaveLobbyCallbackInfo info = {EOS_Success, ClientData, "DirectIP_Lobby"}; CompletionDelegate(&info); } } __declspec(dllexport) uint64_t EOS_Lobby_AddNotifyLobbyMemberStatusReceived( EOS_HLobby Handle, const void* Options, void* ClientData, EOS_Lobby_OnLobbyMemberStatusReceivedCallback NotificationFn) { Log("[Proxy] EOS_Lobby_AddNotifyLobbyMemberStatusReceived called cb=%p", (void*)NotificationFn); g_memberStatusCb = NotificationFn; g_memberStatusCbData = ClientData; if (AtomicRead(&g_lobbyHasRemoteMember) && NotificationFn) { Log("[Proxy] Remote member already present, immediately firing status"); FireLobbyMemberJoined(RemoteProductUserId()); } return g_notifIdCounter++; } __declspec(dllexport) void EOS_Lobby_RemoveNotifyLobbyMemberStatusReceived(EOS_HLobby Handle, uint64_t InId) { Log("[Proxy] EOS_Lobby_RemoveNotifyLobbyMemberStatusReceived id=%llu", (unsigned long long)InId); } __declspec(dllexport) uint64_t EOS_Lobby_AddNotifyLobbyMemberUpdateReceived( EOS_HLobby Handle, const void *Options, void *ClientData, void *NotificationFn) { Log("[Proxy] EOS_Lobby_AddNotifyLobbyMemberUpdateReceived stored cb=%p", NotificationFn); g_memberUpdateCb = (EOS_Lobby_OnLobbyMemberUpdateReceivedCallback)NotificationFn; g_memberUpdateCbData = ClientData; return g_notifIdCounter++; } __declspec(dllexport) void EOS_Lobby_RemoveNotifyLobbyMemberUpdateReceived(EOS_HLobby Handle, uint64_t InId) { Log("[Proxy] EOS_Lobby_RemoveNotifyLobbyMemberUpdateReceived id=%llu", (unsigned long long)InId); } __declspec(dllexport) int32_t EOS_Lobby_IsRTCRoomConnected( EOS_HLobby Handle, const void *Options, bool *bIsRTCRoomConnected) { if (bIsRTCRoomConnected) *bIsRTCRoomConnected = false; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Lobby_GetRTCRoomName(EOS_HLobby Handle, const void *Options, char *OutBuffer, uint32_t *InOutBufferLength) { Log("[Proxy] EOS_Lobby_GetRTCRoomName called"); if (!OutBuffer || !InOutBufferLength) return EOS_InvalidParameters; uint32_t needed = 1; if (*InOutBufferLength < needed) { *InOutBufferLength = needed; return EOS_InvalidParameters; } OutBuffer[0] = '\0'; *InOutBufferLength = needed; return EOS_Success; } // some lobby functions __declspec(dllexport) EOS_EResult EOS_Lobby_IsLobbyOwner(EOS_HLobby Handle, const void* Options, int32_t* bIsLobbyOwner) { Log("[Proxy] EOS_Lobby_IsLobbyOwner called"); if (bIsLobbyOwner) *bIsLobbyOwner = AtomicRead(&g_isHost) ? 1 : 0; return EOS_Success; } __declspec(dllexport) uint32_t EOS_Lobby_GetInviteCount(EOS_HLobby Handle, const void* Options) { Log("[Proxy] EOS_Lobby_GetInviteCount called -> 0"); return 0; } __declspec(dllexport) EOS_EResult EOS_Lobby_CopyInviteIdByIndex(EOS_HLobby Handle, const void* Options, char* OutBuffer, int32_t* InOutBufferLength) { Log("[Proxy] EOS_Lobby_CopyInviteIdByIndex called"); return EOS_NotFound; } __declspec(dllexport) EOS_EResult EOS_Lobby_GetConnectString(EOS_HLobby Handle, const void* Options, char* OutBuffer, uint32_t* InOutBufferLength) { Log("[Proxy] EOS_Lobby_GetConnectString called"); if (!OutBuffer || !InOutBufferLength) return EOS_InvalidParameters; const char* connectStr = "DirectIP_Lobby_Connect"; uint32_t needed = (uint32_t)strlen(connectStr) + 1; if (*InOutBufferLength < needed) { *InOutBufferLength = needed; return EOS_InvalidParameters; } memcpy(OutBuffer, connectStr, needed); *InOutBufferLength = needed; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Lobby_ParseConnectString(EOS_HLobby Handle, const void* Options, char* OutLobbyId, uint32_t* InOutBufferLength) { Log("[Proxy] EOS_Lobby_ParseConnectString called"); if (!OutLobbyId || !InOutBufferLength) return EOS_InvalidParameters; const char* lobbyId = "DirectIP_Lobby"; uint32_t needed = (uint32_t)strlen(lobbyId) + 1; if (*InOutBufferLength < needed) { *InOutBufferLength = needed; return EOS_InvalidParameters; } memcpy(OutLobbyId, lobbyId, needed); *InOutBufferLength = needed; return EOS_Success; } // Platform mocks typedef void* EOS_HPlatform; __declspec(dllexport) EOS_EResult EOS_Platform_GetActiveCountryCode(EOS_HPlatform Handle, const void* Options, char* OutBuffer, int32_t* InOutBufferLength) { if (!OutBuffer || !InOutBufferLength) return EOS_InvalidParameters; const char* country = "US"; int32_t needed = (int32_t)strlen(country) + 1; if (*InOutBufferLength < needed) { *InOutBufferLength = needed; return EOS_InvalidParameters; } memcpy(OutBuffer, country, needed); *InOutBufferLength = needed; return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Platform_GetActiveLocaleCode(EOS_HPlatform Handle, const void* Options, char* OutBuffer, int32_t* InOutBufferLength) { if (!OutBuffer || !InOutBufferLength) return EOS_InvalidParameters; const char* locale = "en"; int32_t needed = (int32_t)strlen(locale) + 1; if (*InOutBufferLength < needed) { *InOutBufferLength = needed; return EOS_InvalidParameters; } memcpy(OutBuffer, locale, needed); *InOutBufferLength = needed; return EOS_Success; } typedef void* EOS_HPlayerDataStorage; struct EOS_PlayerDataStorage_QueryFileOptions { int32_t ApiVersion; EOS_ProductUserId LocalUserId; const char* Filename; }; struct EOS_PlayerDataStorage_QueryFileCallbackInfo { EOS_EResult ResultCode; void* ClientData; EOS_ProductUserId LocalUserId; const char* Filename; }; typedef void (*EOS_PlayerDataStorage_OnQueryFileCompleteCallback)(const EOS_PlayerDataStorage_QueryFileCallbackInfo* Data); __declspec(dllexport) void EOS_PlayerDataStorage_QueryFile( EOS_HPlayerDataStorage Handle, const EOS_PlayerDataStorage_QueryFileOptions* Options, void* ClientData, const EOS_PlayerDataStorage_OnQueryFileCompleteCallback CompletionCallback) { const char* fname = (Options && Options->Filename) ? Options->Filename : "(null)"; Log("[Proxy] EOS_PlayerDataStorage_QueryFile called file=%s (answered locally, not forwarded to real SDK)", fname); if (CompletionCallback) { EOS_PlayerDataStorage_QueryFileCallbackInfo info = {0}; info.ResultCode = EOS_NotFound; // no real cloud backend -- behave like an empty save slot info.ClientData = ClientData; info.LocalUserId = Options ? Options->LocalUserId : LocalProductUserId(); info.Filename = fname; CompletionCallback(&info); } } typedef void* EOS_HSanctions; struct EOS_Sanctions_QueryActivePlayerSanctionsOptions { int32_t ApiVersion; EOS_ProductUserId LocalUserId; EOS_ProductUserId TargetUserId; }; struct EOS_Sanctions_QueryActivePlayerSanctionsCallbackInfo { EOS_EResult ResultCode; void* ClientData; EOS_ProductUserId LocalUserId; EOS_ProductUserId TargetUserId; }; typedef void (*EOS_Sanctions_OnQueryActivePlayerSanctionsCallback)(const EOS_Sanctions_QueryActivePlayerSanctionsCallbackInfo* Data); __declspec(dllexport) void EOS_Sanctions_QueryActivePlayerSanctions( EOS_HSanctions Handle, const EOS_Sanctions_QueryActivePlayerSanctionsOptions* Options, void* ClientData, const EOS_Sanctions_OnQueryActivePlayerSanctionsCallback CompletionDelegate) { Log("[Proxy] EOS_Sanctions_QueryActivePlayerSanctions called (answered locally, not forwarded to real SDK)"); if (CompletionDelegate) { EOS_Sanctions_QueryActivePlayerSanctionsCallbackInfo info = {0}; info.ResultCode = EOS_Success; // no sanctions to report info.ClientData = ClientData; info.LocalUserId = Options ? Options->LocalUserId : LocalProductUserId(); info.TargetUserId = Options ? Options->TargetUserId : LocalProductUserId(); CompletionDelegate(&info); } } struct EOS_Sanctions_GetPlayerSanctionCountOptions { int32_t ApiVersion; EOS_ProductUserId TargetUserId; }; __declspec(dllexport) uint32_t EOS_Sanctions_GetPlayerSanctionCount( EOS_HSanctions Handle, const EOS_Sanctions_GetPlayerSanctionCountOptions* Options) { Log("[Proxy] EOS_Sanctions_GetPlayerSanctionCount called -> 0"); return 0; } struct EOS_Sanctions_CopyPlayerSanctionByIndexOptions { int32_t ApiVersion; EOS_ProductUserId TargetUserId; uint32_t SanctionIndex; }; __declspec(dllexport) EOS_EResult EOS_Sanctions_CopyPlayerSanctionByIndex( EOS_HSanctions Handle, const EOS_Sanctions_CopyPlayerSanctionByIndexOptions* Options, void** OutSanction) { Log("[Proxy] EOS_Sanctions_CopyPlayerSanctionByIndex called -> NotFound"); if (OutSanction) *OutSanction = nullptr; return EOS_NotFound; } struct EOS_LogMessage { const char* Category; const char* Message; int32_t Level; // EOS_ELogLevel }; typedef void (*EOS_LogMessageFunc)(const EOS_LogMessage* Message); // EOS_ELogLevel #define EOS_LOG_Off 0 #define EOS_LOG_Fatal 1 #define EOS_LOG_Error 2 #define EOS_LOG_Warning 3 #define EOS_LOG_Info 4 #define EOS_LOG_Verbose 5 #define EOS_LOG_VeryVerbose 6 // EOS_ELogCategory #define EOS_LC_ALL_CATEGORIES 0x7FFFFFFF typedef EOS_EResult (*EOS_Logging_SetCallback_Fn)(EOS_LogMessageFunc Callback); typedef EOS_EResult (*EOS_Logging_SetLogLevel_Fn)(int32_t LogCategory, int32_t LogLevel); static EOS_Logging_SetCallback_Fn Original_EOS_Logging_SetCallback = nullptr; static EOS_Logging_SetLogLevel_Fn Original_EOS_Logging_SetLogLevel = nullptr; static EOS_LogMessageFunc g_gameLogCallback = nullptr; static LONG g_sdkLogCount = 0; static const char* LogLevelToStr(int32_t level) { switch (level) { case EOS_LOG_Fatal: return "FATAL"; case EOS_LOG_Error: return "ERROR"; case EOS_LOG_Warning: return "WARN"; case EOS_LOG_Info: return "INFO"; case EOS_LOG_Verbose: return "VERBOSE"; case EOS_LOG_VeryVerbose: return "VVERBOSE"; default: return "?"; } } void ProxySdkLogCallback(const EOS_LogMessage* Message) { if (Message) { InterlockedIncrement(&g_sdkLogCount); Log("[SDK/%s] %s: %s", Message->Category ? Message->Category : "?", LogLevelToStr(Message->Level), Message->Message ? Message->Message : ""); } if (g_gameLogCallback) g_gameLogCallback(Message); } __declspec(dllexport) EOS_EResult EOS_Logging_SetCallback(EOS_LogMessageFunc Callback) { Log("[Proxy] EOS_Logging_SetCallback called by game, cb=%p (chaining through proxy sink)", (void*)Callback); g_gameLogCallback = Callback; if (Original_EOS_Logging_SetCallback) { return Original_EOS_Logging_SetCallback(ProxySdkLogCallback); } return EOS_Success; } __declspec(dllexport) EOS_EResult EOS_Logging_SetLogLevel(int32_t LogCategory, int32_t LogLevel) { Log("[Proxy] EOS_Logging_SetLogLevel called by game: category=%d level=%d (forwarding as-is)", LogCategory, LogLevel); if (Original_EOS_Logging_SetLogLevel) { return Original_EOS_Logging_SetLogLevel(LogCategory, LogLevel); } return EOS_Success; } // EOS_Platform_Tick typedef void (*EOS_Platform_Tick_Fn)(void* Handle); static EOS_Platform_Tick_Fn Original_EOS_Platform_Tick = nullptr; static LONG g_tickCount = 0; __declspec(dllexport) void EOS_Platform_Tick(void* Handle) { if (Original_EOS_Platform_Tick) { Original_EOS_Platform_Tick(Handle); } LONG tick = InterlockedIncrement(&g_tickCount); size_t pending = 0; { EnterCriticalSection(&g_callbackQueueCs); pending = g_callbackQueue.size(); LeaveCriticalSection(&g_callbackQueueCs); } if (pending > 0) { Log("[Tick] #%ld: %zu callback(s) pending, draining now", (long)tick, pending); } if (tick % 300 == 1) { DWORD now = GetTickCount(); DWORD msSinceGoodPkt = (g_lastGoodPacketTime && g_lastGoodPacketTime != -1) ? (now - g_lastGoodPacketTime) : (DWORD)-1; Log("[Tick] heartbeat #%ld (isHost=%d initialized=%d peerConnected=%d " "lobbyHasRemoteMember=%d supp=%lu msSinceGoodPkt=%lu socket=%s)", (long)tick, (int)AtomicRead(&g_isHost), (int)AtomicRead(&g_initialized), (int)AtomicRead(&g_peerConnected), (int)AtomicRead(&g_lobbyHasRemoteMember), (unsigned long)AtomicRead(&g_autoTeardownSuppressed), (unsigned long)msSinceGoodPkt, (udpSocket != INVALID_SOCKET) ? "open" : "closed"); } DrainCallbackQueue(); TickRetryPendingEvents(); } } // extern "C" #include "proxy_pointers.h" HMODULE hOriginalDll = NULL; void InitLogFiles() { InitializeCriticalSection(&g_packetCs); char path[MAX_PATH]; GetModuleFileNameA(NULL, path, MAX_PATH); char *lastSlash = strrchr(path, '\\'); if (!lastSlash) lastSlash = strrchr(path, '/'); if (lastSlash) *(lastSlash + 1) = '\0'; else path[0] = '\0'; char logPath[MAX_PATH]; char pktPath[MAX_PATH]; strcpy(logPath, path); strcat(logPath, "proxy_p2p.log"); strcpy(pktPath, path); strcat(pktPath, "proxy_packets.log"); logFile = fopen(logPath, "a"); packetFile = fopen(pktPath, "a"); if (logFile) { setvbuf(logFile, NULL, _IOFBF, 1 << 16); SYSTEMTIME st; GetLocalTime(&st); fprintf(logFile, "\n=========================================\n"); fprintf(logFile, "Proxy DLL attached at %02u:%02u:%02u\n", st.wHour, st.wMinute, st.wSecond); fflush(logFile); // flush this one banner line immediately so attach is visible even if it crashes right after } if (packetFile) { SYSTEMTIME st; GetLocalTime(&st); fprintf(packetFile, "\n=========================================\n"); fprintf(packetFile, "Packet dump started at %02u:%02u:%02u\n", st.wHour, st.wMinute, st.wSecond); fflush(packetFile); } } void CloseLogFiles() { if (logFile) { fclose(logFile); logFile = NULL; } if (packetFile) { fclose(packetFile); packetFile = NULL; } DeleteCriticalSection(&g_packetCs); } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { DisableThreadLibraryCalls(hModule); InitializeCriticalSection(&g_hostEventCs); InitializeCriticalSection(&g_socketCs); InitCallbackQueue(); g_stopEvent = CreateEvent(NULL, TRUE, FALSE, NULL); char path[MAX_PATH]; GetModuleFileNameA(hModule, path, MAX_PATH); strncpy(g_dllDir, path, MAX_PATH); char *dirSlash = strrchr(g_dllDir, '\\'); if (!dirSlash) dirSlash = strrchr(g_dllDir, '/'); if (dirSlash) *(dirSlash + 1) = '\0'; else g_dllDir[0] = '\0'; InitLogFiles(); char *lastSlash = strrchr(path, '\\'); if (!lastSlash) lastSlash = strrchr(path, '/'); if (lastSlash) { *(lastSlash + 1) = '\0'; strcat(path, "EOSSDK-Win64-Shipping_orig.dll"); } else { strcpy(path, "EOSSDK-Win64-Shipping_orig.dll"); } Log("[Proxy] DLL Loaded. Redirecting P2P to UDP."); Log("[Proxy] DLL directory: %s", g_dllDir); Log("[Proxy] Trying to load original: %s", path); hOriginalDll = LoadLibraryA(path); if (hOriginalDll) { LoadOriginalFunctions(hOriginalDll); Original_EOS_Platform_Tick = (EOS_Platform_Tick_Fn)GetProcAddress(hOriginalDll, "EOS_Platform_Tick"); Original_EOS_Logging_SetCallback = (EOS_Logging_SetCallback_Fn)GetProcAddress(hOriginalDll, "EOS_Logging_SetCallback"); Original_EOS_Logging_SetLogLevel = (EOS_Logging_SetLogLevel_Fn)GetProcAddress(hOriginalDll, "EOS_Logging_SetLogLevel"); if (Original_EOS_Logging_SetCallback) { EOS_EResult r = Original_EOS_Logging_SetCallback(ProxySdkLogCallback); Log("[Proxy] Registered proxy SDK log sink, result=%d", r); } else { Log("[Proxy] !! EOS_Logging_SetCallback not found in original DLL -- no internal SDK logs available"); } if (Original_EOS_Logging_SetLogLevel) { EOS_EResult r = Original_EOS_Logging_SetLogLevel(EOS_LC_ALL_CATEGORIES, EOS_LOG_VeryVerbose); Log("[Proxy] Forced SDK log level to VeryVerbose (all categories), result=%d", r); } else { Log("[Proxy] !! EOS_Logging_SetLogLevel not found in original DLL -- log verbosity left at default"); } Log("[Proxy] Original DLL loaded successfully."); } else { Log("[Proxy] Failed to load original DLL! Error: %d", GetLastError()); } break; } case DLL_PROCESS_DETACH: if (AtomicRead(&g_initialized)) WSACleanup(); Log("[Proxy] DLL detaching. Cleaning up."); if (g_stopEvent) { SetEvent(g_stopEvent); CloseHandle(g_stopEvent); g_stopEvent = NULL; } ShutdownCallbackQueue(); DeleteCriticalSection(&g_hostEventCs); DeleteCriticalSection(&g_socketCs); CloseLogFiles(); if (hOriginalDll) FreeLibrary(hOriginalDll); break; } return TRUE; }