diff --git a/Minecraft.Client/Windows64/Windows64_LceLiveRelay.cpp b/Minecraft.Client/Windows64/Windows64_LceLiveRelay.cpp index 5b0f094c..de3657ad 100644 --- a/Minecraft.Client/Windows64/Windows64_LceLiveRelay.cpp +++ b/Minecraft.Client/Windows64/Windows64_LceLiveRelay.cpp @@ -216,19 +216,57 @@ namespace // Per-direction forwarding thread params // ------------------------------------------------------------------------- + // ------------------------------------------------------------------------- + // Self-managing forwarding session. + // Created when forwarding starts; owned jointly by the two forwarding + // threads via a reference count. The last thread to exit closes all + // handles and deletes the session, so the g_relay singleton can reset + // to Idle immediately and accept the next player. + // ------------------------------------------------------------------------- + + struct DetachedForwardingSession + { + HINTERNET wsHandle = nullptr; + HINTERNET wsSession = nullptr; + HINTERNET wsConnect = nullptr; + SOCKET tcpSocket = INVALID_SOCKET; + std::atomic stop{false}; + CRITICAL_SECTION wsSendLock; + std::atomic refCount{2}; // two forwarding threads + + // Called by each thread on exit. The last one cleans up. + void Release() + { + if (--refCount == 0) + { + if (wsHandle) + { + WinHttpWebSocketClose(wsHandle, + WINHTTP_WEB_SOCKET_SUCCESS_CLOSE_STATUS, nullptr, 0); + WinHttpCloseHandle(wsHandle); + } + if (wsConnect) WinHttpCloseHandle(wsConnect); + if (wsSession) WinHttpCloseHandle(wsSession); + if (tcpSocket != INVALID_SOCKET) closesocket(tcpSocket); + DeleteCriticalSection(&wsSendLock); + LCELOG("RELAY", "forwarding session closed"); + delete this; + } + } + }; + struct ForwardWsToTcpParams { - HINTERNET wsHandle; - SOCKET tcpSocket; - std::atomic* stop; + HINTERNET wsHandle; + SOCKET tcpSocket; + DetachedForwardingSession* session; }; struct ForwardTcpToWsParams { - SOCKET tcpSocket; - HINTERNET wsHandle; - CRITICAL_SECTION* wsSendLock; - std::atomic* stop; + SOCKET tcpSocket; + HINTERNET wsHandle; + DetachedForwardingSession* session; }; // ------------------------------------------------------------------------- @@ -237,10 +275,11 @@ namespace DWORD WINAPI ForwardWsToTcpProc(LPVOID param) { - auto* p = static_cast(param); + auto* p = static_cast(param); + auto* session = p->session; std::vector buf(65536); - while (!p->stop->load()) + while (!session->stop.load()) { DWORD bytesRead = 0; WINHTTP_WEB_SOCKET_BUFFER_TYPE bufType = WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE; @@ -266,17 +305,20 @@ namespace // Write all bytes to the TCP socket. const char* src = reinterpret_cast(buf.data()); DWORD remaining = bytesRead; - while (remaining > 0 && !p->stop->load()) + while (remaining > 0 && !session->stop.load()) { const int sent = send(p->tcpSocket, src, static_cast(remaining), 0); - if (sent <= 0) { p->stop->store(true); break; } + if (sent <= 0) { session->stop.store(true); break; } src += sent; remaining -= static_cast(sent); } } - p->stop->store(true); + // Signal the other direction to stop, then release our reference. + session->stop.store(true); + shutdown(p->tcpSocket, SD_RECEIVE); // unblocks recv() in TCP→WS thread delete p; + session->Release(); return 0; } @@ -286,29 +328,31 @@ namespace DWORD WINAPI ForwardTcpToWsProc(LPVOID param) { - auto* p = static_cast(param); + auto* p = static_cast(param); + auto* session = p->session; std::vector buf(65536); - while (!p->stop->load()) + while (!session->stop.load()) { const int received = recv(p->tcpSocket, buf.data(), static_cast(buf.size()), 0); if (received <= 0) break; - EnterCriticalSection(p->wsSendLock); + EnterCriticalSection(&session->wsSendLock); const DWORD err = WinHttpWebSocketSend( p->wsHandle, WINHTTP_WEB_SOCKET_BINARY_MESSAGE_BUFFER_TYPE, buf.data(), static_cast(received)); - LeaveCriticalSection(p->wsSendLock); + LeaveCriticalSection(&session->wsSendLock); if (err != ERROR_SUCCESS) break; } - p->stop->store(true); + session->stop.store(true); delete p; + session->Release(); return 0; } @@ -401,23 +445,44 @@ namespace void StartForwarding() { - g_relay.stopForwarding.store(false); + // Create a self-managing session that owns the WS and TCP handles. + // The two forwarding threads share it via refCount; the last one to + // exit closes all handles and deletes the session. + auto* session = new DetachedForwardingSession(); + session->wsHandle = g_relay.wsHandle; + session->wsSession = g_relay.wsSession; + session->wsConnect = g_relay.wsConnect; + session->tcpSocket = g_relay.tcpSocket; + session->stop.store(false); + session->refCount.store(2); + InitializeCriticalSection(&session->wsSendLock); - auto* wsToTcpP = new ForwardWsToTcpParams(); - wsToTcpP->wsHandle = g_relay.wsHandle; - wsToTcpP->tcpSocket = g_relay.tcpSocket; - wsToTcpP->stop = &g_relay.stopForwarding; + // Transfer handle ownership out of the singleton so it can be reused. + g_relay.wsHandle = nullptr; + g_relay.wsSession = nullptr; + g_relay.wsConnect = nullptr; + g_relay.tcpSocket = INVALID_SOCKET; + g_relay.wsToTcpThread = nullptr; + g_relay.tcpToWsThread = nullptr; - auto* tcpToWsP = new ForwardTcpToWsParams(); - tcpToWsP->tcpSocket = g_relay.tcpSocket; - tcpToWsP->wsHandle = g_relay.wsHandle; - tcpToWsP->wsSendLock = &g_relay.wsSendLock; - tcpToWsP->stop = &g_relay.stopForwarding; + auto* wsToTcpP = new ForwardWsToTcpParams(); + wsToTcpP->wsHandle = session->wsHandle; + wsToTcpP->tcpSocket = session->tcpSocket; + wsToTcpP->session = session; - g_relay.wsToTcpThread = CreateThread(nullptr, 0, ForwardWsToTcpProc, wsToTcpP, 0, nullptr); - g_relay.tcpToWsThread = CreateThread(nullptr, 0, ForwardTcpToWsProc, tcpToWsP, 0, nullptr); + auto* tcpToWsP = new ForwardTcpToWsParams(); + tcpToWsP->tcpSocket = session->tcpSocket; + tcpToWsP->wsHandle = session->wsHandle; + tcpToWsP->session = session; - g_relay.state = Win64LceLiveRelay::ERelayState::Relaying; + HANDLE t1 = CreateThread(nullptr, 0, ForwardWsToTcpProc, wsToTcpP, 0, nullptr); + HANDLE t2 = CreateThread(nullptr, 0, ForwardTcpToWsProc, tcpToWsP, 0, nullptr); + if (t1) CloseHandle(t1); // detached — session manages its own lifetime + if (t2) CloseHandle(t2); + + // Singleton resets to Idle immediately so the next HostOpen() can start + // for the next player without waiting for this session to finish. + g_relay.state = Win64LceLiveRelay::ERelayState::Idle; LCELOG("RELAY", "forwarding active — data flowing"); } diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 28847ad3..fe1b9aff 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -1870,6 +1870,11 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, // skipped entirely, leaving the host's WS to time out and drop the relay). { static Win64LceLiveP2P::EP2PState s_lastP2PState = Win64LceLiveP2P::EP2PState::Idle; + // Set when a session is consumed and a fresh SIG+relay pair is needed. + // Unlike s_lastP2PState this is NOT overwritten each tick, so it persists + // until the edge trigger actually fires (IsActive + sigState==Idle may not + // both be true in the same tick that the recycle runs). + static bool s_needsNewSession = false; const Win64LceLiveP2P::P2PSnapshot p2pSnap = Win64LceLiveP2P::GetP2PSnapshot(); if (p2pSnap.state == Win64LceLiveP2P::EP2PState::Ready) @@ -1877,28 +1882,44 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, const Win64LceLiveSignaling::ESignalingState sigState = Win64LceLiveSignaling::GetSnapshot().state; + const Win64LceLiveRelay::RelaySnapshot relaySnap = + Win64LceLiveRelay::GetSnapshot(); + + // Case 1: Signaling completed (or failed) — direct P2P path or server-closed. + // Recycle SIG and relay so the next invite gets a fresh session ID. if (WinsockNetLayer::IsHosting() && (sigState == Win64LceLiveSignaling::ESignalingState::PeerKnown || sigState == Win64LceLiveSignaling::ESignalingState::Failed || sigState == Win64LceLiveSignaling::ESignalingState::Closed)) { - // Recycle stale/consumed signaling + relay sessions so the next invite - // gets a fresh pair. P2P stays up — the external IP/port mapping - // remains valid for the duration of the host session. Win64LceLiveSignaling::Close(); - Win64LceLiveRelay::Close(); - // Reset the edge-trigger so the Ready→HostConnect path re-fires on - // the next frame (P2P is still Ready, so without this reset the - // trigger would never fire again and the second invite would stall). - s_lastP2PState = Win64LceLiveP2P::EP2PState::Idle; + Win64LceLiveRelay::Close(); // no-op if relay already auto-detached + s_needsNewSession = true; } - // Host: edge-trigger on first Ready frame (requires active session). + // Case 2: Player joined via relay without completing signaling (CGNAT path). + // The server closes the joiner WS immediately (code 4317) when both sides + // are behind symmetric NAT — signaling is pointless, relay is the only path. + // The host SIG stays Connected forever; PeerKnown/Closed never fires. + // Detect: SIG still Connected + relay auto-detached to Idle + player in game. + if (WinsockNetLayer::IsHosting() && + WinsockNetLayer::IsActive() && + sigState == Win64LceLiveSignaling::ESignalingState::Connected && + relaySnap.state == Win64LceLiveRelay::ERelayState::Idle) + { + Win64LceLiveSignaling::Close(); // relay is already Idle, no Close() needed + s_needsNewSession = true; + } + + // Host: fire when P2P first becomes Ready OR when a session was consumed + // and we need a new one. IsActive() guard ensures the game is actually + // hosting before we open a new signaling/relay slot. if (WinsockNetLayer::IsHosting() && WinsockNetLayer::IsActive() && - s_lastP2PState != Win64LceLiveP2P::EP2PState::Ready && + (s_lastP2PState != Win64LceLiveP2P::EP2PState::Ready || s_needsNewSession) && sigState == Win64LceLiveSignaling::ESignalingState::Idle) { + s_needsNewSession = false; Win64LceLiveSignaling::HostConnect( p2pSnap.externalIp, p2pSnap.externalPort, p2pSnap.connMethod);