Refactor async server joining with eJoinState enum and dedicated progress UI

Replace the boolean-flag-based async join system with a clean state machine
(eJoinState enum) and move connection progress handling from UIScene_JoinMenu
into UIScene_ConnectingProgress as a dedicated UI class.

Combines the best of two approaches: non-blocking sockets with select()
timeout and SO_RCVTIMEO clearing (prevents random disconnects) with the
upstream's state enum, FinalizeJoin separation, and ConnectingProgress UI.

JoinGame() now returns JOINGAME_PENDING on Win64, and
PlatformNetworkManagerStub::DoWork() polls the join state to finalize
the connection when the background thread succeeds.
This commit is contained in:
itsRevela
2026-03-26 11:51:17 -05:00
parent 39b0ad1cb4
commit f1310abe08
10 changed files with 414 additions and 204 deletions
@@ -773,31 +773,10 @@ void CGameNetworkManager::CancelJoinGame(LPVOID lpParam)
s_pPlatformNetworkManager->CancelJoinGame();
#endif
#ifdef _WINDOWS64
static_cast<CPlatformNetworkManagerStub*>(s_pPlatformNetworkManager)->CancelJoinGame();
WinsockNetLayer::CancelJoinGame();
#endif
}
#ifdef _WINDOWS64
bool CGameNetworkManager::BeginJoinGameAsync(FriendSessionInfo *searchResult, int localUsersMask)
{
app.SetTutorialMode(false);
g_NetworkManager.SetLocalGame(false);
int primaryUserIndex = ProfileManager.GetLockedProfile();
Minecraft::GetInstance()->clearConnectionFailed();
localUsersMask |= GetLocalPlayerMask(ProfileManager.GetPrimaryPad());
return static_cast<CPlatformNetworkManagerStub*>(s_pPlatformNetworkManager)->BeginJoinGameAsync(searchResult, localUsersMask, primaryUserIndex);
}
int CGameNetworkManager::FinishJoinGame(FriendSessionInfo *searchResult)
{
return static_cast<CPlatformNetworkManagerStub*>(s_pPlatformNetworkManager)->FinishJoinGame(searchResult);
}
#endif
bool CGameNetworkManager::LeaveGame(bool bMigrateHost)
{
Minecraft::GetInstance()->gui->clearMessages();
@@ -47,7 +47,8 @@ public:
{
JOINGAME_SUCCESS,
JOINGAME_FAIL_GENERAL,
JOINGAME_FAIL_SERVER_FULL
JOINGAME_FAIL_SERVER_FULL,
JOINGAME_PENDING
} eJoinGameResult;
void Initialise();
@@ -106,10 +107,6 @@ public:
bool JoinGameFromInviteInfo( int userIndex, int userMask, const INVITE_INFO *pInviteInfo);
eJoinGameResult JoinGame(FriendSessionInfo *searchResult, int localUsersMask);
static void CancelJoinGame(LPVOID lpParam); // Not part of the shared interface
#ifdef _WINDOWS64
bool BeginJoinGameAsync(FriendSessionInfo *searchResult, int localUsersMask);
int FinishJoinGame(FriendSessionInfo *searchResult);
#endif
bool LeaveGame(bool bMigrateHost);
static int JoinFromInvite_SignInReturned(void *pParam,bool bContinue, int iPad);
void UpdateAndSetGameSessionData(INetworkPlayer *pNetworkPlayerLeaving = nullptr);
@@ -173,6 +173,9 @@ bool CPlatformNetworkManagerStub::Initialise(CGameNetworkManager *pGameNetworkMa
m_bSearchPending = false;
m_bIsOfflineGame = false;
#ifdef _WINDOWS64
m_bJoinPending = false;
#endif
m_pSearchParam = nullptr;
m_SessionsUpdatedCallback = nullptr;
@@ -282,6 +285,39 @@ void CPlatformNetworkManagerStub::DoWork()
m_bLeaveGameOnTick = false;
}
}
// Async join finalization: when the background thread reports success,
// register players and transition the session to starting state.
if (m_bJoinPending)
{
WinsockNetLayer::eJoinState state = WinsockNetLayer::GetJoinState();
if (state == WinsockNetLayer::eJoinState_Success)
{
WinsockNetLayer::FinalizeJoin();
BYTE localSmallId = WinsockNetLayer::GetLocalSmallId();
IQNet::m_player[localSmallId].m_smallId = localSmallId;
IQNet::m_player[localSmallId].m_isRemote = false;
IQNet::m_player[localSmallId].m_isHostPlayer = false;
IQNet::m_player[localSmallId].m_resolvedXuid = Win64Xuid::ResolvePersistentXuid();
Minecraft* pMinecraft = Minecraft::GetInstance();
wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str());
IQNet::s_playerCount = localSmallId + 1;
NotifyPlayerJoined(&IQNet::m_player[0]);
NotifyPlayerJoined(&IQNet::m_player[localSmallId]);
m_pGameNetworkManager->StateChange_AnyToStarting();
m_bJoinPending = false;
}
else if (state == WinsockNetLayer::eJoinState_Failed ||
state == WinsockNetLayer::eJoinState_Rejected ||
state == WinsockNetLayer::eJoinState_Cancelled)
{
m_bJoinPending = false;
}
}
#endif
}
@@ -517,90 +553,19 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l
WinsockNetLayer::StopDiscovery();
if (!WinsockNetLayer::JoinGame(hostIP, hostPort))
if (!WinsockNetLayer::BeginJoinGame(hostIP, hostPort))
{
app.DebugPrintf("Win64 LAN: Failed to connect to %s:%d\n", hostIP, hostPort);
app.DebugPrintf("Win64 LAN: Failed to start async join to %s:%d\n", hostIP, hostPort);
return CGameNetworkManager::JOINGAME_FAIL_GENERAL;
}
BYTE localSmallId = WinsockNetLayer::GetLocalSmallId();
IQNet::m_player[localSmallId].m_smallId = localSmallId;
IQNet::m_player[localSmallId].m_isRemote = false;
IQNet::m_player[localSmallId].m_isHostPlayer = false;
// Local non-host identity is the persistent uid.dat XUID.
IQNet::m_player[localSmallId].m_resolvedXuid = Win64Xuid::ResolvePersistentXuid();
Minecraft* pMinecraft = Minecraft::GetInstance();
wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str());
IQNet::s_playerCount = localSmallId + 1;
NotifyPlayerJoined(&IQNet::m_player[0]);
NotifyPlayerJoined(&IQNet::m_player[localSmallId]);
m_pGameNetworkManager->StateChange_AnyToStarting();
return CGameNetworkManager::JOINGAME_SUCCESS;
m_bJoinPending = true;
return CGameNetworkManager::JOINGAME_PENDING;
#else
return CGameNetworkManager::JOINGAME_SUCCESS;
#endif
}
#ifdef _WINDOWS64
bool CPlatformNetworkManagerStub::BeginJoinGameAsync(FriendSessionInfo* searchResult, int localUsersMask, int primaryUserIndex)
{
if (searchResult == nullptr)
return false;
const char* hostIP = searchResult->data.hostIP;
int hostPort = searchResult->data.hostPort;
if (hostPort <= 0 || hostIP[0] == 0)
return false;
m_bLeavingGame = false;
m_bLeaveGameOnTick = false;
IQNet::s_isHosting = false;
m_pIQNet->ClientJoinGame();
IQNet::m_player[0].m_smallId = 0;
IQNet::m_player[0].m_isRemote = true;
IQNet::m_player[0].m_isHostPlayer = true;
IQNet::m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid();
wcsncpy_s(IQNet::m_player[0].m_gamertag, 32, searchResult->data.hostName, _TRUNCATE);
WinsockNetLayer::StopDiscovery();
return WinsockNetLayer::StartJoinGameAsync(hostIP, hostPort);
}
int CPlatformNetworkManagerStub::FinishJoinGame(FriendSessionInfo* searchResult)
{
BYTE localSmallId = WinsockNetLayer::GetLocalSmallId();
IQNet::m_player[localSmallId].m_smallId = localSmallId;
IQNet::m_player[localSmallId].m_isRemote = false;
IQNet::m_player[localSmallId].m_isHostPlayer = false;
IQNet::m_player[localSmallId].m_resolvedXuid = Win64Xuid::ResolvePersistentXuid();
Minecraft* pMinecraft = Minecraft::GetInstance();
wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str());
IQNet::s_playerCount = localSmallId + 1;
NotifyPlayerJoined(&IQNet::m_player[0]);
NotifyPlayerJoined(&IQNet::m_player[localSmallId]);
m_pGameNetworkManager->StateChange_AnyToStarting();
return CGameNetworkManager::JOINGAME_SUCCESS;
}
void CPlatformNetworkManagerStub::CancelJoinGame()
{
WinsockNetLayer::CancelJoinGame();
}
#endif
bool CPlatformNetworkManagerStub::SetLocalGame(bool isLocal)
{
m_bIsOfflineGame = isLocal;
@@ -42,11 +42,6 @@ public:
virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0);
virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex );
#ifdef _WINDOWS64
bool BeginJoinGameAsync(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex);
int FinishJoinGame(FriendSessionInfo *searchResult);
void CancelJoinGame();
#endif
virtual bool SetLocalGame(bool isLocal);
virtual bool IsLocalGame() { return m_bIsOfflineGame; }
virtual void SetPrivateGame(bool isPrivate);
@@ -81,6 +76,9 @@ private:
bool m_bIsOfflineGame;
bool m_bIsPrivateGame;
int m_flagIndexSize;
#ifdef _WINDOWS64
bool m_bJoinPending;
#endif
// This is only maintained by the host, and is not valid on client machines
GameSessionData m_hostGameSessionData;
@@ -2,6 +2,15 @@
#include "UI.h"
#include "UIScene_ConnectingProgress.h"
#include "..\..\Minecraft.h"
#ifdef _WINDOWS64
#include "..\..\Windows64\Network\WinsockNetLayer.h"
static int ConnectingProgress_OnRejectedDialogOK(LPVOID, int iPad, const C4JStorage::EMessageResult)
{
ui.NavigateBack(iPad);
return 0;
}
#endif
UIScene_ConnectingProgress::UIScene_ConnectingProgress(int iPad, void *_initData, UILayer *parentLayer) : UIScene(iPad, parentLayer)
{
@@ -43,6 +52,12 @@ UIScene_ConnectingProgress::UIScene_ConnectingProgress(int iPad, void *_initData
m_cancelFuncParam = param->cancelFuncParam;
m_removeLocalPlayer = false;
m_showingButton = false;
#ifdef _WINDOWS64
WinsockNetLayer::eJoinState initState = WinsockNetLayer::GetJoinState();
m_asyncJoinActive = (initState == WinsockNetLayer::eJoinState_Connecting ||
initState == WinsockNetLayer::eJoinState_Success);
m_asyncJoinFailed = false;
#endif
}
UIScene_ConnectingProgress::~UIScene_ConnectingProgress()
@@ -53,6 +68,18 @@ UIScene_ConnectingProgress::~UIScene_ConnectingProgress()
void UIScene_ConnectingProgress::updateTooltips()
{
#ifdef _WINDOWS64
if (m_asyncJoinActive)
{
ui.SetTooltips(m_iPad, -1, IDS_TOOLTIPS_BACK);
return;
}
if (m_asyncJoinFailed)
{
ui.SetTooltips(m_iPad, IDS_TOOLTIPS_SELECT, -1);
return;
}
#endif
// 4J-PB - removing the option of cancel join, since it didn't work anyway
//ui.SetTooltips( m_iPad, -1, m_showTooltips?IDS_TOOLTIPS_CANCEL_JOIN:-1);
ui.SetTooltips( m_iPad, -1, -1);
@@ -62,6 +89,61 @@ void UIScene_ConnectingProgress::tick()
{
UIScene::tick();
#ifdef _WINDOWS64
if (m_asyncJoinActive)
{
WinsockNetLayer::eJoinState state = WinsockNetLayer::GetJoinState();
switch (state)
{
case WinsockNetLayer::eJoinState_Connecting:
{
int attempt = WinsockNetLayer::GetJoinAttempt();
int maxAttempts = WinsockNetLayer::GetJoinMaxAttempts();
wchar_t buf[128];
if (attempt > 1)
swprintf_s(buf, L"Connecting... (attempt %d/%d)", attempt, maxAttempts);
else
swprintf_s(buf, L"Connecting...");
m_labelTitle.setLabel(buf);
break;
}
case WinsockNetLayer::eJoinState_Success:
m_asyncJoinActive = false;
m_labelTitle.setLabel(L"Joining world...");
break;
case WinsockNetLayer::eJoinState_Cancelled:
m_asyncJoinActive = false;
navigateBack();
break;
case WinsockNetLayer::eJoinState_Rejected:
{
m_asyncJoinActive = false;
DisconnectPacket::eDisconnectReason reason = WinsockNetLayer::GetJoinRejectReason();
int reasonStringId = IDS_CONNECTION_LOST_SERVER;
if (reason == DisconnectPacket::eDisconnect_ServerFull)
reasonStringId = IDS_DISCONNECTED_SERVER_FULL;
else if (reason == DisconnectPacket::eDisconnect_Kicked)
reasonStringId = IDS_DISCONNECTED_KICKED;
UINT uiIDA[1];
uiIDA[0] = IDS_CONFIRM_OK;
ui.RequestErrorMessage(IDS_CONNECTION_FAILED, reasonStringId, uiIDA, 1, m_iPad, ConnectingProgress_OnRejectedDialogOK, nullptr);
break;
}
case WinsockNetLayer::eJoinState_Failed:
m_asyncJoinActive = false;
m_asyncJoinFailed = true;
m_labelTitle.setLabel(app.GetString(IDS_CONNECTION_FAILED));
m_buttonConfirm.setVisible(true);
m_showingButton = true;
updateTooltips();
break;
default:
break;
}
}
#endif
if( m_removeLocalPlayer )
{
m_removeLocalPlayer = false;
@@ -94,6 +176,7 @@ void UIScene_ConnectingProgress::handleGainFocus(bool navBack)
void UIScene_ConnectingProgress::handleLoseFocus()
{
if (!m_runFailTimer) return;
int millisecsLeft = getTimer(0)->targetTime - System::currentTimeMillis();
int millisecsTaken = getTimer(0)->duration - millisecsLeft;
app.DebugPrintf("\n");
@@ -207,6 +290,18 @@ void UIScene_ConnectingProgress::handleInput(int iPad, int key, bool repeat, boo
switch(key)
{
#ifdef _WINDOWS64
case ACTION_MENU_CANCEL:
if (pressed && m_asyncJoinActive)
{
m_asyncJoinActive = false;
WinsockNetLayer::CancelJoinGame();
navigateBack();
handled = true;
return;
}
break;
#endif
// 4J-PB - Removed the option to cancel join - it didn't work anyway
// case ACTION_MENU_CANCEL:
// {
@@ -250,6 +345,13 @@ void UIScene_ConnectingProgress::handlePress(F64 controlId, F64 childId)
case eControl_Confirm:
if(m_showingButton)
{
#ifdef _WINDOWS64
if (m_asyncJoinFailed)
{
navigateBack();
break;
}
#endif
if( m_iPad != ProfileManager.GetPrimaryPad() && g_NetworkManager.IsInSession() )
{
// The connection failed if we see the button, so the temp player should be removed and the viewports updated again
@@ -12,6 +12,10 @@ private:
bool m_showingButton;
void (*m_cancelFunc)(LPVOID param);
LPVOID m_cancelFuncParam;
#ifdef _WINDOWS64
bool m_asyncJoinActive;
bool m_asyncJoinFailed;
#endif
enum EControls
{
+17 -56
View File
@@ -29,8 +29,6 @@ UIScene_JoinMenu::UIScene_JoinMenu(int iPad, void *_initData, UILayer *parentLay
m_editServerPhase = eEditServer_Idle;
m_editServerButtonIndex = -1;
m_deleteServerButtonIndex = -1;
m_asyncJoinInProgress = false;
m_joinLocalUsersMask = 0;
#endif
}
@@ -64,35 +62,6 @@ void UIScene_JoinMenu::updateTooltips()
void UIScene_JoinMenu::tick()
{
#ifdef _WINDOWS64
if (m_asyncJoinInProgress && WinsockNetLayer::IsJoinComplete())
{
m_asyncJoinInProgress = false;
if (WinsockNetLayer::GetJoinResult())
{
int result = g_NetworkManager.FinishJoinGame(m_selectedSession);
app.SetLiveLinkRequired(false);
if (result != CGameNetworkManager::JOINGAME_SUCCESS)
{
m_bIgnoreInput = false;
m_buttonJoinGame.setLabel(app.GetString(IDS_JOIN_GAME));
updateTooltips();
}
}
else
{
app.SetLiveLinkRequired(false);
m_bIgnoreInput = false;
m_buttonJoinGame.setLabel(app.GetString(IDS_JOIN_GAME));
updateTooltips();
UINT uiIDA[1];
uiIDA[0] = IDS_CONFIRM_OK;
ui.RequestErrorMessage(IDS_CONNECTION_FAILED, IDS_CONNECTION_LOST_SERVER, uiIDA, 1, ProfileManager.GetPrimaryPad());
}
}
#endif
if( !m_friendInfoRequestIssued )
{
ui.NavigateToScene(m_iPad, eUIScene_Timer);
@@ -316,19 +285,6 @@ wstring UIScene_JoinMenu::getMoviePath()
void UIScene_JoinMenu::handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled)
{
#ifdef _WINDOWS64
if (m_asyncJoinInProgress && key == ACTION_MENU_CANCEL && pressed)
{
g_NetworkManager.CancelJoinGame(nullptr);
m_asyncJoinInProgress = false;
m_bIgnoreInput = false;
m_buttonJoinGame.setLabel(app.GetString(IDS_JOIN_GAME));
updateTooltips();
handled = true;
return;
}
#endif
if(m_bIgnoreInput) return;
ui.AnimateKeyPress(m_iPad, key, repeat, pressed, released);
@@ -625,23 +581,28 @@ void UIScene_JoinMenu::JoinGame(UIScene_JoinMenu* pClass)
ProfileManager.DisplaySystemMessage( SCE_MSG_DIALOG_SYSMSG_TYPE_TRC_PSN_CHAT_RESTRICTION, ProfileManager.GetPrimaryPad() );
}
#endif
#ifdef _WINDOWS64
if (g_NetworkManager.BeginJoinGameAsync(pClass->m_selectedSession, dwLocalUsersMask))
{
pClass->m_asyncJoinInProgress = true;
pClass->m_joinLocalUsersMask = dwLocalUsersMask;
pClass->m_buttonJoinGame.setLabel(app.GetString(IDS_PROGRESS_CONNECTING));
ui.SetTooltips(DEFAULT_XUI_MENU_USER, -1, IDS_TOOLTIPS_CANCEL_JOIN);
return;
}
CGameNetworkManager::eJoinGameResult result = CGameNetworkManager::JOINGAME_FAIL_GENERAL;
#else
CGameNetworkManager::eJoinGameResult result = g_NetworkManager.JoinGame( pClass->m_selectedSession, dwLocalUsersMask );
#endif
// Alert the app the we no longer want to be informed of ethernet connections
app.SetLiveLinkRequired( false );
#ifdef _WINDOWS64
if (result == CGameNetworkManager::JOINGAME_PENDING)
{
pClass->m_bIgnoreInput = false;
ConnectionProgressParams *param = new ConnectionProgressParams();
param->iPad = ProfileManager.GetPrimaryPad();
param->stringId = IDS_PROGRESS_CONNECTING;
param->showTooltips = true;
param->setFailTimer = false;
param->timerTime = 0;
param->cancelFunc = nullptr;
param->cancelFuncParam = nullptr;
ui.NavigateToScene(ProfileManager.GetPrimaryPad(), eUIScene_ConnectingProgress, param);
return;
}
#endif
if( result != CGameNetworkManager::JOINGAME_SUCCESS )
{
int exitReasonStringId = -1;
@@ -70,8 +70,6 @@ private:
wstring m_editServerPort;
int m_editServerButtonIndex;
int m_deleteServerButtonIndex;
bool m_asyncJoinInProgress;
DWORD m_joinLocalUsersMask;
#endif
public: