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
This commit is contained in:
Sean Hoyt
2026-03-01 10:50:48 -06:00
committed by GitHub
parent d5707899db
commit b5111232aa
7 changed files with 95 additions and 5 deletions

View File

@@ -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;

View File

@@ -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

View File

@@ -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
{

View File

@@ -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
}
}
}

View File

@@ -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) {}

View File

@@ -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) ) )

View File

@@ -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"