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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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) ) )
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user