aboutsummaryrefslogtreecommitdiff
path: root/Minecraft.Client
diff options
context:
space:
mode:
authorSean Hoyt <seanhoyt963@gmail.com>2026-03-01 10:50:48 -0600
committerGitHub <noreply@github.com>2026-03-02 00:50:48 +0800
commitb5111232aa13952f58ed1b3b3525ea825662b95c (patch)
tree2e7450af7d442c6337e33bba0d81f5cb38622806 /Minecraft.Client
parentd5707899dbc99d5c9520095246fc0bf2425c6235 (diff)
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
Diffstat (limited to 'Minecraft.Client')
-rw-r--r--Minecraft.Client/ClientConnection.cpp17
-rw-r--r--Minecraft.Client/Common/Consoles_App.cpp4
-rw-r--r--Minecraft.Client/Common/UI/UIScene_CreateWorldMenu.cpp13
-rw-r--r--Minecraft.Client/Common/UI/UIScene_LoadMenu.cpp7
-rw-r--r--Minecraft.Client/Extrax64Stubs.cpp28
-rw-r--r--Minecraft.Client/Minecraft.cpp30
-rw-r--r--Minecraft.Client/stdafx.h1
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<AddPlayerPacket> 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<AddPlayerPacket> 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 <Xinput.h>
#include "Windows64\4JLibs\inc\4J_Input.h"
#include "Windows64\4JLibs\inc\4J_Profile.h"
#include "Windows64\4JLibs\inc\4J_Render.h"