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:
@@ -734,6 +734,21 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
|
|||||||
return;
|
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 x = packet->x / 32.0;
|
||||||
double y = packet->y / 32.0;
|
double y = packet->y / 32.0;
|
||||||
|
|||||||
@@ -8956,7 +8956,11 @@ bool CMinecraftApp::IsLocalMultiplayerAvailable()
|
|||||||
if( InputManager.IsPadConnected(i) || ProfileManager.IsSignedIn(i) ) ++connectedControllers;
|
if( InputManager.IsPadConnected(i) || ProfileManager.IsSignedIn(i) ) ++connectedControllers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef _WINDOWS64
|
||||||
|
bool available = connectedControllers > 1;
|
||||||
|
#else
|
||||||
bool available = RenderManager.IsHiDef() && connectedControllers > 1;
|
bool available = RenderManager.IsHiDef() && connectedControllers > 1;
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __ORBIS__
|
#ifdef __ORBIS__
|
||||||
// Check for remote play
|
// Check for remote play
|
||||||
|
|||||||
@@ -993,12 +993,17 @@ void UIScene_CreateWorldMenu::checkStateAndStartGame()
|
|||||||
#endif
|
#endif
|
||||||
else
|
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());
|
//ProfileManager.RequestSignInUI(false, false, false, true, false,&CScene_MultiGameCreate::StartGame_SignInReturned, this,ProfileManager.GetPrimaryPad());
|
||||||
SignInInfo info;
|
SignInInfo info;
|
||||||
info.Func = &UIScene_CreateWorldMenu::StartGame_SignInReturned;
|
info.Func = &UIScene_CreateWorldMenu::StartGame_SignInReturned;
|
||||||
info.lpParam = this;
|
info.lpParam = this;
|
||||||
info.requireOnline = m_MoreOptionsParams.bOnlineGame;
|
info.requireOnline = m_MoreOptionsParams.bOnlineGame;
|
||||||
ui.NavigateToScene(ProfileManager.GetPrimaryPad(),eUIScene_QuadrantSignin,&info);
|
ui.NavigateToScene(ProfileManager.GetPrimaryPad(),eUIScene_QuadrantSignin,&info);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1355,12 +1360,18 @@ int UIScene_CreateWorldMenu::ConfirmCreateReturned(void *pParam,int iPad,C4JStor
|
|||||||
|
|
||||||
if(isClientSide && app.IsLocalMultiplayerAvailable())
|
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());
|
//ProfileManager.RequestSignInUI(false, false, false, true, false,&UIScene_CreateWorldMenu::StartGame_SignInReturned, pClass,ProfileManager.GetPrimaryPad());
|
||||||
SignInInfo info;
|
SignInInfo info;
|
||||||
info.Func = &UIScene_CreateWorldMenu::StartGame_SignInReturned;
|
info.Func = &UIScene_CreateWorldMenu::StartGame_SignInReturned;
|
||||||
info.lpParam = pClass;
|
info.lpParam = pClass;
|
||||||
info.requireOnline = pClass->m_MoreOptionsParams.bOnlineGame;
|
info.requireOnline = pClass->m_MoreOptionsParams.bOnlineGame;
|
||||||
ui.NavigateToScene(ProfileManager.GetPrimaryPad(),eUIScene_QuadrantSignin,&info);
|
ui.NavigateToScene(ProfileManager.GetPrimaryPad(),eUIScene_QuadrantSignin,&info);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1337,7 +1337,14 @@ int UIScene_LoadMenu::LoadDataComplete(void *pParam)
|
|||||||
#endif
|
#endif
|
||||||
else
|
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;
|
pClass->m_bRequestQuadrantSignin = true;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,7 +197,15 @@ bool IQNetPlayer::IsHost() { return this == &IQNet::m_player[0]; }
|
|||||||
bool IQNetPlayer::IsGuest() { return false; }
|
bool IQNetPlayer::IsGuest() { return false; }
|
||||||
bool IQNetPlayer::IsLocal() { return true; }
|
bool IQNetPlayer::IsLocal() { return true; }
|
||||||
PlayerUID IQNetPlayer::GetXuid() { return INVALID_XUID; }
|
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; }
|
int IQNetPlayer::GetSessionIndex() { return 0; }
|
||||||
bool IQNetPlayer::IsTalking() { return false; }
|
bool IQNetPlayer::IsTalking() { return false; }
|
||||||
bool IQNetPlayer::IsMutedByLocalUser(DWORD dwUserIndex) { 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); }
|
void SetFakeGamertag(char *name){ strcpy_s(fakeGamerTag, name); }
|
||||||
char* C_4JProfile::GetGamertag(int iPad){ return fakeGamerTag; }
|
char* C_4JProfile::GetGamertag(int iPad){ return fakeGamerTag; }
|
||||||
#else
|
#else
|
||||||
char* C_4JProfile::GetGamertag(int iPad){ return "PlayerName"; }
|
char* C_4JProfile::GetGamertag(int iPad)
|
||||||
wstring C_4JProfile::GetDisplayName(int iPad){ return L"PlayerName"; }
|
{
|
||||||
|
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
|
#endif
|
||||||
bool C_4JProfile::IsFullVersion() { return s_bProfileIsFullVersion; }
|
bool C_4JProfile::IsFullVersion() { return s_bProfileIsFullVersion; }
|
||||||
void C_4JProfile::SetSignInChangeCallback(void ( *Func)(LPVOID, bool, unsigned int),LPVOID lpParam) {}
|
void C_4JProfile::SetSignInChangeCallback(void ( *Func)(LPVOID, bool, unsigned int),LPVOID lpParam) {}
|
||||||
|
|||||||
@@ -1564,7 +1564,31 @@ void Minecraft::run_middle()
|
|||||||
// 4J Stu - This doesn't make any sense with the way we handle XboxOne users
|
// 4J Stu - This doesn't make any sense with the way we handle XboxOne users
|
||||||
#ifndef _DURANGO
|
#ifndef _DURANGO
|
||||||
// did we just get input from a player who doesn't exist? They'll be wanting to join the game then
|
// 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);
|
bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && RenderManager.IsHiDef() && InputManager.ButtonPressed(i);
|
||||||
|
#endif
|
||||||
#ifdef __ORBIS__
|
#ifdef __ORBIS__
|
||||||
// Check for remote play
|
// Check for remote play
|
||||||
tryJoin = tryJoin && InputManager.IsLocalMultiplayerAvailable();
|
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
|
// did we just get input from a player who doesn't exist? They'll be wanting to join the game then
|
||||||
#ifdef __ORBIS__
|
#ifdef __ORBIS__
|
||||||
if(InputManager.ButtonPressed(i, ACTION_MENU_A))
|
if(InputManager.ButtonPressed(i, ACTION_MENU_A))
|
||||||
|
#elif defined _WINDOWS64
|
||||||
|
if(startJustPressed)
|
||||||
#else
|
#else
|
||||||
if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_PAUSEMENU))
|
if(InputManager.ButtonPressed(i, MINECRAFT_ACTION_PAUSEMENU))
|
||||||
#endif
|
#endif
|
||||||
@@ -1599,7 +1625,11 @@ void Minecraft::run_middle()
|
|||||||
// Let them join
|
// Let them join
|
||||||
|
|
||||||
// are they signed in?
|
// are they signed in?
|
||||||
|
#ifdef _WINDOWS64
|
||||||
|
if(ProfileManager.IsSignedIn(i) || (g_NetworkManager.IsLocalGame() && InputManager.IsPadConnected(i)))
|
||||||
|
#else
|
||||||
if(ProfileManager.IsSignedIn(i))
|
if(ProfileManager.IsSignedIn(i))
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
// if this is a local game, then the player just needs to be signed in
|
// 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) ) )
|
if( g_NetworkManager.IsLocalGame() || (ProfileManager.IsSignedInLive(i) && ProfileManager.AllowedToPlayMultiplayer(i) ) )
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ typedef XUID GameSessionUID;
|
|||||||
#include "Durango\4JLibs\inc\4J_Render.h"
|
#include "Durango\4JLibs\inc\4J_Render.h"
|
||||||
#include "Durango\4JLibs\inc\4J_Storage.h"
|
#include "Durango\4JLibs\inc\4J_Storage.h"
|
||||||
#elif defined _WINDOWS64
|
#elif defined _WINDOWS64
|
||||||
|
#include <Xinput.h>
|
||||||
#include "Windows64\4JLibs\inc\4J_Input.h"
|
#include "Windows64\4JLibs\inc\4J_Input.h"
|
||||||
#include "Windows64\4JLibs\inc\4J_Profile.h"
|
#include "Windows64\4JLibs\inc\4J_Profile.h"
|
||||||
#include "Windows64\4JLibs\inc\4J_Render.h"
|
#include "Windows64\4JLibs\inc\4J_Render.h"
|
||||||
|
|||||||
Reference in New Issue
Block a user