diff --git a/README.md b/README.md index 540151c..d240a0b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,25 @@ +### This project is being worked on and not everything works yet, contact me for help + + + # EOSSDK-Holos -A emulation layer of EOSSDK that does it's best to enable true p2p multiplayer \ No newline at end of file +EOSSDK-Holos is an emulation layer for the Epic Online Services (EOS) SDK. Its primary objective is to make possible true Peer-to-Peer multiplayer connectivity by intercepting and redirecting P2P traffic over a direct UDP socket. + + +# Build + +1. Place the contents of this repository into the game's plugin directory: + +`Holo's Hanafuda\HolosHanafuda_Data\Plugins\x86_64\` + + +The project acts as a DLL proxy. To ensure the game can still access the original SDK functions, you must rename the existing file: + + +2. Locate `EOSSDK-Win64-Shipping.dll` in the directory mentioned above and rename it to `EOSSDK-Win64-Shipping_orig.dll`. + + +Execute the build script provided in the repository to generate the proxy DLL: + +```bash ./build.sh``` \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..ee8a98c --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +python3 gen_def.py +python3 gen_stubs.py + +x86_64-w64-mingw32-g++ -shared -o EOSSDK-Win64-Shipping.dll main.cpp proxy_asm.s proxy.def -lws2_32 -static \ No newline at end of file diff --git a/gen_def.py b/gen_def.py new file mode 100644 index 0000000..7fb6f62 --- /dev/null +++ b/gen_def.py @@ -0,0 +1,136 @@ +import re +import sys + +def main(): + with open('dump.txt', 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + # Find the [Ordinal/Name Pointer] Table + match = re.search(r'\[Ordinal/Name Pointer\] Table.*?\n(.*?)(\n\n|\Z)', content, re.DOTALL) + if not match: + print("Could not find Name Pointer Table in dump.txt") + return + + lines = match.group(1).split('\n') + + hooks = [ + "EOS_P2P_SendPacket", + "EOS_P2P_GetNextReceivedPacketSize", + "EOS_P2P_ReceivePacket", + "EOS_P2P_AcceptConnection", + "EOS_P2P_CloseConnection", + "EOS_P2P_CloseConnections", + "EOS_P2P_QueryNATType", + "EOS_P2P_GetNATType", + "EOS_P2P_SetRelayControl", + "EOS_P2P_GetRelayControl", + "EOS_P2P_SetPortRange", + "EOS_P2P_GetPortRange", + "EOS_P2P_AddNotifyPeerConnectionRequest", + "EOS_P2P_RemoveNotifyPeerConnectionRequest", + "EOS_P2P_AddNotifyPeerConnectionClosed", + "EOS_P2P_RemoveNotifyPeerConnectionClosed", + "EOS_P2P_AddNotifyPeerConnectionEstablished", + "EOS_P2P_RemoveNotifyPeerConnectionEstablished", + "EOS_P2P_AddNotifyPeerConnectionInterrupted", + "EOS_P2P_RemoveNotifyPeerConnectionInterrupted", + "EOS_P2P_AddNotifyIncomingPacketQueueFull", + "EOS_P2P_RemoveNotifyIncomingPacketQueueFull", + "EOS_P2P_ClearPacketQueue", + "EOS_P2P_GetPacketQueueInfo", + "EOS_P2P_SetPacketQueueSize", + "EOS_Auth_Login", + "EOS_Connect_Login", + "EOS_Connect_CreateUser", + "EOS_EpicAccountId_FromString", + "EOS_ProductUserId_FromString", + "EOS_EpicAccountId_IsValid", + "EOS_ProductUserId_IsValid", + "EOS_ProductUserId_ToString", + "EOS_Connect_AddNotifyAuthExpiration", + "EOS_Lobby_CreateLobby", + "EOS_Lobby_JoinLobby", + "EOS_Lobby_CreateLobbySearch", + "EOS_LobbySearch_Find", + "EOS_LobbySearch_GetSearchResultCount", + "EOS_LobbySearch_CopySearchResultByIndex", + "EOS_LobbyDetails_CopyInfo", + "EOS_LobbyDetails_GetLobbyOwner", + "EOS_LobbyDetails_Release", + "EOS_LobbySearch_Release", + "EOS_Lobby_AddNotifyLobbyUpdateReceived", + "EOS_Lobby_RemoveNotifyLobbyUpdateReceived", + "EOS_LobbySearch_RemoveParameter", + "EOS_LobbySearch_SetLobbyId", + "EOS_LobbySearch_SetMaxResults", + "EOS_LobbySearch_SetParameter", + "EOS_LobbySearch_SetTargetUserId", + "EOS_LobbyModification_AddAttribute", + "EOS_LobbyModification_AddMemberAttribute", + "EOS_LobbyModification_Release", + "EOS_LobbyModification_RemoveAttribute", + "EOS_LobbyModification_RemoveMemberAttribute", + "EOS_LobbyModification_SetAllowedPlatformIds", + "EOS_LobbyModification_SetBucketId", + "EOS_LobbyModification_SetInvitesAllowed", + "EOS_LobbyModification_SetMaxMembers", + "EOS_LobbyModification_SetPermissionLevel", + "EOS_Lobby_UpdateLobbyModification", + "EOS_LobbyDetails_CopyAttributeByIndex", + "EOS_LobbyDetails_CopyAttributeByKey", + "EOS_LobbyDetails_CopyMemberAttributeByIndex", + "EOS_LobbyDetails_CopyMemberAttributeByKey", + "EOS_LobbyDetails_CopyMemberInfo", + "EOS_LobbyDetails_GetAttributeCount", + "EOS_LobbyDetails_GetMemberAttributeCount", + "EOS_LobbyDetails_GetMemberByIndex", + "EOS_LobbyDetails_GetMemberCount", + "EOS_LobbyDetails_Info_Release", + "EOS_LobbyDetails_MemberInfo_Release", + "EOS_Lobby_CopyLobbyDetailsHandle", + "EOS_Lobby_CopyLobbyDetailsHandleByInviteId", + "EOS_Lobby_CopyLobbyDetailsHandleByUiEventId", + "EOS_Lobby_UpdateLobby", + "EOS_Lobby_DestroyLobby", + "EOS_Lobby_LeaveLobby", + "EOS_Lobby_AddNotifyLobbyMemberStatusReceived", + "EOS_Lobby_RemoveNotifyLobbyMemberStatusReceived", + "EOS_Lobby_AddNotifyLobbyMemberUpdateReceived", + "EOS_Lobby_RemoveNotifyLobbyMemberUpdateReceived", + "EOS_Lobby_IsRTCRoomConnected", + "EOS_Lobby_GetRTCRoomName", + "EOS_Lobby_Attribute_Release", + "EOS_Connect_CopyIdToken", + "EOS_Connect_IdToken_Release", + "EOS_Connect_VerifyIdToken", + "EOS_Platform_Tick", + "EOS_Lobby_IsLobbyOwner", + "EOS_Lobby_GetInviteCount", + "EOS_Lobby_CopyInviteIdByIndex", + "EOS_Lobby_GetConnectString", + "EOS_Lobby_ParseConnectString", + "EOS_Platform_GetActiveCountryCode", + "EOS_Platform_GetActiveLocaleCode", + ] + + with open('proxy.def', 'w') as out: + out.write('LIBRARY "EOSSDK-Win64-Shipping.dll"\n') + out.write('EXPORTS\n') + for line in lines: + line = line.strip() + if not line: continue + # Parse line: [ 0] +base[ 1] 0000 EOS_Achievements_AddNotifyAchievementsUnlocked + parts = line.split() + if len(parts) >= 4: + func_name = parts[-1] + if func_name in hooks: + # Export our hook directly + out.write(f' {func_name}\n') + else: + # Forward to the original dll + out.write(f' {func_name}=EOSSDK-Win64-Shipping_orig.{func_name}\n') + + print("proxy.def generated successfully.") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/gen_stubs.py b/gen_stubs.py new file mode 100644 index 0000000..e0cd57a --- /dev/null +++ b/gen_stubs.py @@ -0,0 +1,149 @@ +import re + +def main(): + with open('dump.txt', 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + match = re.search(r'\[Ordinal/Name Pointer\] Table.*?\n(.*?)(\n\n|\Z)', content, re.DOTALL) + if not match: + print("Could not find Name Pointer Table in dump.txt") + return + + lines = match.group(1).split('\n') + + hooks = [ + "EOS_P2P_SendPacket", + "EOS_P2P_GetNextReceivedPacketSize", + "EOS_P2P_ReceivePacket", + "EOS_P2P_AcceptConnection", + "EOS_P2P_CloseConnection", + "EOS_P2P_CloseConnections", + "EOS_P2P_QueryNATType", + "EOS_P2P_GetNATType", + "EOS_P2P_SetRelayControl", + "EOS_P2P_GetRelayControl", + "EOS_P2P_SetPortRange", + "EOS_P2P_GetPortRange", + "EOS_P2P_AddNotifyPeerConnectionRequest", + "EOS_P2P_RemoveNotifyPeerConnectionRequest", + "EOS_P2P_AddNotifyPeerConnectionClosed", + "EOS_P2P_RemoveNotifyPeerConnectionClosed", + "EOS_P2P_AddNotifyPeerConnectionEstablished", + "EOS_P2P_RemoveNotifyPeerConnectionEstablished", + "EOS_P2P_AddNotifyPeerConnectionInterrupted", + "EOS_P2P_RemoveNotifyPeerConnectionInterrupted", + "EOS_P2P_AddNotifyIncomingPacketQueueFull", + "EOS_P2P_RemoveNotifyIncomingPacketQueueFull", + "EOS_P2P_ClearPacketQueue", + "EOS_P2P_GetPacketQueueInfo", + "EOS_P2P_SetPacketQueueSize", + "EOS_Auth_Login", + "EOS_Connect_Login", + "EOS_Connect_CreateUser", + "EOS_EpicAccountId_FromString", + "EOS_ProductUserId_FromString", + "EOS_EpicAccountId_IsValid", + "EOS_ProductUserId_IsValid", + "EOS_ProductUserId_ToString", + "EOS_Connect_AddNotifyAuthExpiration", + "EOS_Lobby_CreateLobby", + "EOS_Lobby_JoinLobby", + "EOS_Lobby_CreateLobbySearch", + "EOS_LobbySearch_Find", + "EOS_LobbySearch_GetSearchResultCount", + "EOS_LobbySearch_CopySearchResultByIndex", + "EOS_LobbyDetails_CopyInfo", + "EOS_LobbyDetails_GetLobbyOwner", + "EOS_LobbyDetails_Release", + "EOS_LobbySearch_Release", + "EOS_Lobby_AddNotifyLobbyUpdateReceived", + "EOS_Lobby_RemoveNotifyLobbyUpdateReceived", + "EOS_LobbySearch_RemoveParameter", + "EOS_LobbySearch_SetLobbyId", + "EOS_LobbySearch_SetMaxResults", + "EOS_LobbySearch_SetParameter", + "EOS_LobbySearch_SetTargetUserId", + "EOS_LobbyModification_AddAttribute", + "EOS_LobbyModification_AddMemberAttribute", + "EOS_LobbyModification_Release", + "EOS_LobbyModification_RemoveAttribute", + "EOS_LobbyModification_RemoveMemberAttribute", + "EOS_LobbyModification_SetAllowedPlatformIds", + "EOS_LobbyModification_SetBucketId", + "EOS_LobbyModification_SetInvitesAllowed", + "EOS_LobbyModification_SetMaxMembers", + "EOS_LobbyModification_SetPermissionLevel", + "EOS_Lobby_UpdateLobbyModification", + "EOS_LobbyDetails_CopyAttributeByIndex", + "EOS_LobbyDetails_CopyAttributeByKey", + "EOS_LobbyDetails_CopyMemberAttributeByIndex", + "EOS_LobbyDetails_CopyMemberAttributeByKey", + "EOS_LobbyDetails_CopyMemberInfo", + "EOS_LobbyDetails_GetAttributeCount", + "EOS_LobbyDetails_GetMemberAttributeCount", + "EOS_LobbyDetails_GetMemberByIndex", + "EOS_LobbyDetails_GetMemberCount", + "EOS_LobbyDetails_Info_Release", + "EOS_LobbyDetails_MemberInfo_Release", + "EOS_Lobby_CopyLobbyDetailsHandle", + "EOS_Lobby_CopyLobbyDetailsHandleByInviteId", + "EOS_Lobby_CopyLobbyDetailsHandleByUiEventId", + "EOS_Lobby_UpdateLobby", + "EOS_Lobby_DestroyLobby", + "EOS_Lobby_LeaveLobby", + "EOS_Lobby_AddNotifyLobbyMemberStatusReceived", + "EOS_Lobby_RemoveNotifyLobbyMemberStatusReceived", + "EOS_Lobby_AddNotifyLobbyMemberUpdateReceived", + "EOS_Lobby_RemoveNotifyLobbyMemberUpdateReceived", + "EOS_Lobby_IsRTCRoomConnected", + "EOS_Lobby_GetRTCRoomName", + "EOS_Lobby_Attribute_Release", + "EOS_Connect_CopyIdToken", + "EOS_Connect_IdToken_Release", + "EOS_Connect_VerifyIdToken", + "EOS_Platform_Tick", + "EOS_Lobby_IsLobbyOwner", + "EOS_Lobby_GetInviteCount", + "EOS_Lobby_CopyInviteIdByIndex", + "EOS_Lobby_GetConnectString", + "EOS_Lobby_ParseConnectString", + "EOS_Platform_GetActiveCountryCode", + "EOS_Platform_GetActiveLocaleCode", + ] + + funcs = [] + for line in lines: + line = line.strip() + if not line: continue + parts = line.split() + if len(parts) >= 4: + funcs.append(parts[-1]) + + with open('proxy_asm.s', 'w') as asm: + asm.write('.text\n') + for f in funcs: + if f not in hooks: + asm.write(f'.globl {f}\n') + asm.write(f'{f}:\n') + asm.write(f' jmp *orig_{f}(%rip)\n\n') + + with open('proxy_pointers.h', 'w') as hdr: + hdr.write('extern "C" {\n') + for f in funcs: + if f not in hooks: + hdr.write(f' void* orig_{f} = nullptr;\n') + hdr.write('}\n\n') + hdr.write('void LoadOriginalFunctions(HMODULE hMod) {\n') + for f in funcs: + if f not in hooks: + hdr.write(f' orig_{f} = (void*)GetProcAddress(hMod, "{f}");\n') + hdr.write('}\n') + + with open('proxy.def', 'w') as out: + out.write('LIBRARY "EOSSDK-Win64-Shipping.dll"\n') + out.write('EXPORTS\n') + for f in funcs: + out.write(f' {f}\n') + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..5e362ba --- /dev/null +++ b/main.cpp @@ -0,0 +1,2084 @@ +#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"); + fflush(logFile); + 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'; +} + +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); + +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"; + +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; + +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; + +// Rate limiting for ResetConnectionState +static DWORD g_lastResetTime = 0; +static LONG g_resetCount = 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 (must be complete before extern "C" functions use them) +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() { + // Rate limit resets to prevent rapid re-initialization loops + DWORD now = GetTickCount(); + if (now - g_lastResetTime < 100) { + if (InterlockedIncrement(&g_resetCount) > 5) { + Log("[Net] ResetConnectionState rate-limited (too many rapid resets)"); + InterlockedDecrement(&g_resetCount); + return; + } + } else { + AtomicWrite(&g_resetCount, 0); + } + g_lastResetTime = now; + + Log("[Net] Resetting connection state for new session"); + + // Signal and wait for watch thread to exit before touching the socket + if (g_peerWatchThread) { + if (g_stopEvent) SetEvent(g_stopEvent); + WaitForSingleObject(g_peerWatchThread, 3000); + // Only close handle if it's still valid + EnterCriticalSection(&g_socketCs); + if (g_peerWatchThread) { + CloseHandle(g_peerWatchThread); + g_peerWatchThread = NULL; + } + LeaveCriticalSection(&g_socketCs); + } + + 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; +} + +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 + } +} + +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) { + InitNetwork(); + Log("[Thread] PeerWatchThread started. isHost=%d", (int)AtomicRead(&g_isHost)); + if (AtomicRead(&g_isHost)) { + int loopCount = 0; + while (loopCount < 400) { + if (WaitForSingleObject(g_stopEvent, 0) == WAIT_OBJECT_0) 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()); + // FIX: Exit after request+member are fired, not waiting for Established + if (AtomicRead(&g_connectionRequestFired) && AtomicRead(&g_lobbyMemberJoinedFired)) { + Log("[Thread] Host bootstrap complete, exiting watch thread"); + break; + } + } + + if (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected)) { + TryFireHostEvents(ClientProductUserId()); + if (AtomicRead(&g_connectionRequestFired) && AtomicRead(&g_lobbyMemberJoinedFired)) { + 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; + + 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; + + 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++; + } + } + Log("[Thread] PeerWatchThread exiting"); + // Clear the thread handle under lock to prevent race with ResetConnectionState + EnterCriticalSection(&g_socketCs); + g_peerWatchThread = NULL; + LeaveCriticalSection(&g_socketCs); + return 0; +} + +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); + + static LONG g_sendLogCount = 0; + if (InterlockedIncrement(&g_sendLogCount) <= 80 || sent == SOCKET_ERROR) { + 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 { + 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) { + InitNetwork(); + 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; + static LONG g_sizeLogCount = 0; + 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"); + InitNetwork(); + 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) { + // 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); + static LONG g_receiveLogCount = 0; + if (InterlockedIncrement(&g_receiveLogCount) <= 80) { + 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"); + QueueCallback([=]() { + 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"); + return EOS_Success; +} + +__declspec(dllexport) EOS_EResult +EOS_P2P_CloseConnections(EOS_HP2P Handle, const void *Options) { + Log("[Proxy] EOS_P2P_CloseConnections called (resetting DirectIP state)"); + 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"); + 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; + if (InterlockedIncrement(&g_addCount) <= 2) + Log("[Proxy] EOS_P2P_AddNotifyPeerConnectionRequest called (count=%d)", (int)AtomicRead(&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)"); + 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 (queued)"); + if (CompletionDelegate) { + QueueCallback([=]() { + 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 (async)"); + CompletionDelegate(&info); + }); + } + Log("[Proxy] << EOS_Auth_Login exit"); +} + +__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; + if (InterlockedIncrement(&g_connectLoginCount) <= 3) + Log("[Proxy] EOS_Connect_Login called (call #%d, queued)", (int)AtomicRead(&g_connectLoginCount)); + if (CompletionDelegate) { + QueueCallback([=]() { + EOS_Connect_LoginCallbackInfo info = {0}; + info.ResultCode = EOS_Success; + info.ClientData = ClientData; + info.LocalUserId = LocalProductUserId(); + Log("[Proxy] Invoking EOS_Connect_Login callback (async) localUser=%p", info.LocalUserId); + CompletionDelegate(&info); + }); + } +} + +__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 (queued)"); + if (CompletionDelegate) { + QueueCallback([=]() { + 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 3; +} + +__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 (queued)"); + if (CompletionDelegate) { + QueueCallback([=]() { + 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_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"); + 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); + } + if (!g_peerWatchThread) { + ResetEvent(g_stopEvent); + g_peerWatchThread = CreateThread(NULL, 0, PeerWatchThread, NULL, 0, NULL); + Log("[Proxy] Started PeerWatchThread from CreateLobby"); + } + if (CompletionDelegate) { + QueueCallback([=]() { + EOS_Lobby_CreateLobbyCallbackInfo info = {0}; + info.ResultCode = EOS_Success; + info.ClientData = ClientData; + info.LobbyId = "DirectIP_Lobby"; + Log("[Proxy] Invoking CreateLobby callback (async)"); + 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"); + InitNetwork(); + if (!AtomicRead(&g_isHost)) AtomicWrite(&g_lobbyHasRemoteMember, 1); + if (!g_peerWatchThread) { + ResetEvent(g_stopEvent); + g_peerWatchThread = CreateThread(NULL, 0, PeerWatchThread, NULL, 0, NULL); + Log("[Proxy] Started PeerWatchThread from JoinLobby"); + } + if (CompletionDelegate) { + QueueCallback([=]() { + EOS_Lobby_JoinLobbyCallbackInfo info = {0}; + info.ResultCode = EOS_Success; + info.ClientData = ClientData; + info.LobbyId = "DirectIP_Lobby"; + Log("[Proxy] Invoking JoinLobby callback (async)"); + CompletionDelegate(&info); + }); + } + LogStateSnapshot("JoinLobby"); + Log("[Proxy] << EOS_Lobby_JoinLobby 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) { + QueueCallback([=]() { + EOS_LobbySearch_FindCallbackInfo info = {0}; + info.ResultCode = EOS_Success; + info.ClientData = ClientData; + Log("[Proxy] Invoking LobbySearch_Find callback (async)"); + 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) { + static LONG g_copyInfoLogCount = 0; + dummyLobbyInfo.AvailableSlots = (AtomicRead(&g_lobbyHasRemoteMember) || AtomicRead(&g_peerConnected)) ? 0 : 1; + 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) { + static LONG g_ownerLogCount = 0; + 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; + static LONG g_memberInfoLogCount = 0; + 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(); + static LONG g_memberByIndexLogCount = 0; + 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; + static LONG g_memberCountLogCount = 0; + 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) { + QueueCallback([=]() { + EOS_Lobby_UpdateLobbyCallbackInfo info = {EOS_Success, ClientData, "DirectIP_Lobby"}; + CompletionDelegate(&info); + }); + } + if (g_lobbyUpdateCb) { + QueueCallback([=]() { + EOS_Lobby_LobbyUpdateReceivedCallbackInfo notif = {0}; + notif.ClientData = g_lobbyUpdateCbData; + notif.LobbyId = "DirectIP_Lobby"; + g_lobbyUpdateCb(¬if); + }); + } + if (g_memberUpdateCb) { + QueueCallback([=]() { + 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"); + ResetConnectionState(); + if (CompletionDelegate) { + QueueCallback([=]() { + 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"); + ResetConnectionState(); + if (CompletionDelegate) { + QueueCallback([=]() { + 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; +} + +// EOS_Platform_Tick +typedef void (*EOS_Platform_Tick_Fn)(void* Handle); +static EOS_Platform_Tick_Fn Original_EOS_Platform_Tick = nullptr; + +__declspec(dllexport) void EOS_Platform_Tick(void* Handle) { + if (Original_EOS_Platform_Tick) { + Original_EOS_Platform_Tick(Handle); + } + DrainCallbackQueue(); +} + +} // 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) { + 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); + } + 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"); + 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; +} \ No newline at end of file