From b5111232aa13952f58ed1b3b3525ea825662b95c Mon Sep 17 00:00:00 2001 From: Sean Hoyt Date: Sun, 1 Mar 2026 10:50:48 -0600 Subject: [PATCH] feat: Windows64 local multiplayer support (#13) - Skip QuadrantSignin (profile selector) on Windows64 in both LoadMenu and CreateWorldMenu, proceeding directly to local play since Xbox Live stubs always return true for IsSignedInLive() - Fix IsLocalMultiplayerAvailable() to not require IsHiDef() on Windows64 - Allow pad-connected players to join without a profile sign-in check - Fix ghost RemotePlayer creation by scanning all local player slots and matching on server-assigned player index rather than controller slot, fixing P3/P4 ghost entities when joining out of controller order - Give each player a unique name (Player 1-4) based on controller index instead of a single shared stub name - Use raw XInput (XInputGetState) for secondary controller join detection, bypassing the 4J toggle system which consumes all button presses before game logic runs; uses a 120-frame latch for a reliable detection window - Add .gitignore for Visual Studio build artifacts and output directories --- Minecraft.Client/ClientConnection.cpp | 17 ++++++++++- Minecraft.Client/Common/Consoles_App.cpp | 4 +++ .../Common/UI/UIScene_CreateWorldMenu.cpp | 13 +++++++- .../Common/UI/UIScene_LoadMenu.cpp | 7 +++++ Minecraft.Client/Extrax64Stubs.cpp | 28 +++++++++++++++-- Minecraft.Client/Minecraft.cpp | 30 +++++++++++++++++++ Minecraft.Client/stdafx.h | 1 + 7 files changed, 95 insertions(+), 5 deletions(-) diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index b80eaed9..5ba3cb85 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -723,7 +723,7 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) // Some remote players could actually be local players that are already added for(unsigned int idx = 0; idx < XUSER_MAX_COUNT; ++idx) { - // need to use the XUID here + // need to use the XUID here PlayerUID playerXUIDOnline = INVALID_XUID, playerXUIDOffline = INVALID_XUID; ProfileManager.GetXUID(idx,&playerXUIDOnline,true); ProfileManager.GetXUID(idx,&playerXUIDOffline,false); @@ -734,6 +734,21 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) return; } } +#ifdef _WINDOWS64 + // On Windows64 all XUIDs are INVALID_XUID so the XUID check above never fires. + // packet->m_playerIndex is the server-assigned sequential index (set via LoginPacket), + // NOT the controller slot — so we must scan all local player slots and match by + // their stored server index rather than using it directly as an array subscript. + for(unsigned int idx = 0; idx < XUSER_MAX_COUNT; ++idx) + { + if(minecraft->localplayers[idx] != NULL && + minecraft->localplayers[idx]->getPlayerIndex() == packet->m_playerIndex) + { + app.DebugPrintf("AddPlayerPacket received for local player (controller %d, server index %d), skipping RemotePlayer creation\n", idx, packet->m_playerIndex); + return; + } + } +#endif double x = packet->x / 32.0; double y = packet->y / 32.0; diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 24819250..b325f3a1 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -8956,7 +8956,11 @@ bool CMinecraftApp::IsLocalMultiplayerAvailable() if( InputManager.IsPadConnected(i) || ProfileManager.IsSignedIn(i) ) ++connectedControllers; } +#ifdef _WINDOWS64 + bool available = connectedControllers > 1; +#else bool available = RenderManager.IsHiDef() && connectedControllers > 1; +#endif #ifdef __ORBIS__ // Check for remote play diff --git a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp index fa41909d..6be67bed 100644 --- a/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp @@ -992,13 +992,18 @@ void UIScene_CreateWorldMenu::checkStateAndStartGame() #endif else - { + { +#ifdef _WINDOWS64 + // On Windows64, Xbox Live is unavailable. Skip QuadrantSignin and start directly. + CreateGame(this, 0); +#else //ProfileManager.RequestSignInUI(false, false, false, true, false,&CScene_MultiGameCreate::StartGame_SignInReturned, this,ProfileManager.GetPrimaryPad()); SignInInfo info; info.Func = &UIScene_CreateWorldMenu::StartGame_SignInReturned; info.lpParam = this; info.requireOnline = m_MoreOptionsParams.bOnlineGame; ui.NavigateToScene(ProfileManager.GetPrimaryPad(),eUIScene_QuadrantSignin,&info); +#endif } } else @@ -1355,12 +1360,18 @@ int UIScene_CreateWorldMenu::ConfirmCreateReturned(void *pParam,int iPad,C4JStor if(isClientSide && app.IsLocalMultiplayerAvailable()) { +#ifdef _WINDOWS64 + // On Windows64, Xbox Live is unavailable. Skip QuadrantSignin and start directly. + CreateGame(pClass, 0); + return 0; +#else //ProfileManager.RequestSignInUI(false, false, false, true, false,&UIScene_CreateWorldMenu::StartGame_SignInReturned, pClass,ProfileManager.GetPrimaryPad()); SignInInfo info; info.Func = &UIScene_CreateWorldMenu::StartGame_SignInReturned; info.lpParam = pClass; info.requireOnline = pClass->m_MoreOptionsParams.bOnlineGame; ui.NavigateToScene(ProfileManager.GetPrimaryPad(),eUIScene_QuadrantSignin,&info); +#endif } else { diff --git a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp index f08fc727..e2cbc2aa 100644 --- a/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp @@ -1337,7 +1337,14 @@ int UIScene_LoadMenu::LoadDataComplete(void *pParam) #endif else { +#ifdef _WINDOWS64 + // On Windows64, IsSignedInLive() returns true as a stub but Xbox Live is + // not available. Skip QuadrantSignin and proceed directly with local play. + DWORD dwLocalUsersMask = CGameNetworkManager::GetLocalPlayerMask(ProfileManager.GetPrimaryPad()); + StartGameFromSave(pClass, dwLocalUsersMask); +#else pClass->m_bRequestQuadrantSignin = true; +#endif } } } diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index 334f0123..f6ad76c1 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -197,7 +197,15 @@ bool IQNetPlayer::IsHost() { return this == &IQNet::m_player[0]; } bool IQNetPlayer::IsGuest() { return false; } bool IQNetPlayer::IsLocal() { return true; } PlayerUID IQNetPlayer::GetXuid() { return INVALID_XUID; } -LPCWSTR IQNetPlayer::GetGamertag() { static const wchar_t *test = L"stub"; return test; } +LPCWSTR IQNetPlayer::GetGamertag() +{ + static wchar_t tags[4][16]; + int idx = GetUserIndex(); + if(idx < 0 || idx >= 4) idx = 0; + mbstowcs(tags[idx], ProfileManager.GetGamertag(idx), 15); + tags[idx][15] = L'\0'; + return tags[idx]; +} int IQNetPlayer::GetSessionIndex() { return 0; } bool IQNetPlayer::IsTalking() { return false; } bool IQNetPlayer::IsMutedByLocalUser(DWORD dwUserIndex) { return false; } @@ -487,8 +495,22 @@ char fakeGamerTag[32] = "PlayerName"; void SetFakeGamertag(char *name){ strcpy_s(fakeGamerTag, name); } char* C_4JProfile::GetGamertag(int iPad){ return fakeGamerTag; } #else -char* C_4JProfile::GetGamertag(int iPad){ return "PlayerName"; } -wstring C_4JProfile::GetDisplayName(int iPad){ return L"PlayerName"; } +char* C_4JProfile::GetGamertag(int iPad) +{ + static char tags[4][16] = { "Player 1", "Player 2", "Player 3", "Player 4" }; + if(iPad >= 0 && iPad < 4) return tags[iPad]; + return tags[0]; +} +wstring C_4JProfile::GetDisplayName(int iPad) +{ + switch(iPad) + { + case 1: return L"Player 2"; + case 2: return L"Player 3"; + case 3: return L"Player 4"; + default: return L"Player 1"; + } +} #endif bool C_4JProfile::IsFullVersion() { return s_bProfileIsFullVersion; } void C_4JProfile::SetSignInChangeCallback(void ( *Func)(LPVOID, bool, unsigned int),LPVOID lpParam) {} diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index ff8346be..3192410d 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -1564,7 +1564,31 @@ void Minecraft::run_middle() // 4J Stu - This doesn't make any sense with the way we handle XboxOne users #ifndef _DURANGO // did we just get input from a player who doesn't exist? They'll be wanting to join the game then +#ifdef _WINDOWS64 + // The 4J toggle system is unreliable here: UIController::handleInput() calls + // ButtonPressed for every ACTION_MENU_* mapped button (which covers all physical + // buttons) before run_middle() runs. Bypass it with raw XInput and own edge detection. + // A latch counter keeps startJustPressed active for ~120 frames after the rising edge + // so the detection window is large enough to be caught reliably. + static WORD s_prevXButtons[XUSER_MAX_COUNT] = {}; + static int s_startPressLatch[XUSER_MAX_COUNT] = {}; + XINPUT_STATE xstate_join; + memset(&xstate_join, 0, sizeof(xstate_join)); + WORD xCurButtons = 0; + if (XInputGetState(i, &xstate_join) == ERROR_SUCCESS) + { + xCurButtons = xstate_join.Gamepad.wButtons; + if ((xCurButtons & XINPUT_GAMEPAD_START) != 0 && (s_prevXButtons[i] & XINPUT_GAMEPAD_START) == 0) + s_startPressLatch[i] = 120; // rising edge: latch for ~120 frames (~2s at 60fps) + else if (s_startPressLatch[i] > 0) + s_startPressLatch[i]--; + s_prevXButtons[i] = xCurButtons; + } + bool startJustPressed = s_startPressLatch[i] > 0; + bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0; +#else bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && RenderManager.IsHiDef() && InputManager.ButtonPressed(i); +#endif #ifdef __ORBIS__ // Check for remote play tryJoin = tryJoin && InputManager.IsLocalMultiplayerAvailable(); @@ -1592,6 +1616,8 @@ void Minecraft::run_middle() // did we just get input from a player who doesn't exist? They'll be wanting to join the game then #ifdef __ORBIS__ if(InputManager.ButtonPressed(i, ACTION_MENU_A)) +#elif defined _WINDOWS64 + if(startJustPressed) #else if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_PAUSEMENU)) #endif @@ -1599,7 +1625,11 @@ void Minecraft::run_middle() // Let them join // are they signed in? +#ifdef _WINDOWS64 + if(ProfileManager.IsSignedIn(i) || (g_NetworkManager.IsLocalGame() && InputManager.IsPadConnected(i))) +#else if(ProfileManager.IsSignedIn(i)) +#endif { // if this is a local game, then the player just needs to be signed in if( g_NetworkManager.IsLocalGame() || (ProfileManager.IsSignedInLive(i) && ProfileManager.AllowedToPlayMultiplayer(i) ) ) diff --git a/Minecraft.Client/stdafx.h b/Minecraft.Client/stdafx.h index 4ae4250e..05b17e81 100644 --- a/Minecraft.Client/stdafx.h +++ b/Minecraft.Client/stdafx.h @@ -175,6 +175,7 @@ typedef XUID GameSessionUID; #include "Durango\4JLibs\inc\4J_Render.h" #include "Durango\4JLibs\inc\4J_Storage.h" #elif defined _WINDOWS64 + #include #include "Windows64\4JLibs\inc\4J_Input.h" #include "Windows64\4JLibs\inc\4J_Profile.h" #include "Windows64\4JLibs\inc\4J_Render.h"