5 Commits

Author SHA1 Message Date
Loki Rautio
90318307e2 Kinda start work on uid saveload 2026-03-05 10:55:59 -06:00
Loki Rautio
66b9e61925 Merge remote-tracking branch 'origin/main' into feat/multiplayer-fix 2026-03-05 10:06:40 -06:00
Loki Rautio
29b0ae7de1 Fallback singleplayer save loading 2026-03-05 10:06:23 -06:00
Loki Rautio
0bb7c2e16d Merge remote-tracking branch 'kuwacom/fix/multiplayer-player-data' into feat/multiplayer-fix 2026-03-05 09:46:50 -06:00
kuwacom
f8cbd4f844 fix: fix multiplayer player data mix between different players bug
Fixes a Win64 multiplayer issue where player data (`players/*.dat`) could be mismatched because identity was effectively tied to connection-order `smallId` XUIDs.
Introduces a deterministic username-derived persistent XUID and integrates it into the existing XUID-based save pipeline.

- Added `Windows64_NameXuid` for deterministic `name -> persistent xuid` resolution
- On Win64 login (`PlayerList`), set `ServerPlayer::xuid` from username-based resolver
- Aligned local player `xuid` assignment (`Minecraft`) for create/init/respawn paths to use the same resolver
- Added Win64 local-self guard in `ClientConnection::handleAddPlayer` using name match to avoid duplicate local remote-player creation
- Kept `IQNet::GetPlayerByXuid` compatibility fallback behavior, while extending lookup to also resolve username-based XUIDs
- Moved implementation to `Minecraft.Client/Windows64/Windows64_NameXuid.h`; kept legacy `Win64NameXuid.h` as compatibility include

Rename migration is intentionally out of scope (same-name identity only).
2026-03-05 12:29:15 +09:00
7 changed files with 143 additions and 15 deletions

View File

@@ -762,6 +762,18 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
return;
}
}
#ifdef _WINDOWS64
// Win64 player-data XUIDs are resolved from player name, so also guard against creating
// a duplicate remote player for a local slot by checking the username directly.
for (unsigned int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
{
if (minecraft->localplayers[idx] != NULL && minecraft->localplayers[idx]->name == packet->name)
{
app.DebugPrintf("AddPlayerPacket received for local player name %ls\n", packet->name.c_str());
return;
}
}
#endif
/*#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),
@@ -3961,4 +3973,4 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr<
{
m_recievedTick = GetTickCount();
m_packet = packet;
}
}

View File

@@ -21,6 +21,7 @@
#include "Windows64\Social\SocialManager.h"
#include "Windows64\Sentient\DynamicConfigurations.h"
#include "Windows64\Network\WinsockNetLayer.h"
#include "Windows64\Windows64_NameXuid.h"
#elif defined __PSVITA__
#include "PSVita\Sentient\SentientManager.h"
#include "StatsCounter.h"
@@ -200,7 +201,7 @@ DWORD IQNetPlayer::GetCurrentRtt() { return 0; }
bool IQNetPlayer::IsHost() { return m_isHostPlayer; }
bool IQNetPlayer::IsGuest() { return false; }
bool IQNetPlayer::IsLocal() { return !m_isRemote; }
PlayerUID IQNetPlayer::GetXuid() { return (PlayerUID)(0xe000d45248242f2e + m_smallId); } // todo: restore to INVALID_XUID once saves support this
PlayerUID IQNetPlayer::GetXuid() { return m_playerUID; }
LPCWSTR IQNetPlayer::GetGamertag() { return m_gamertag; }
int IQNetPlayer::GetSessionIndex() { return m_smallId; }
bool IQNetPlayer::IsTalking() { return false; }
@@ -285,7 +286,12 @@ IQNetPlayer* IQNet::GetPlayerByXuid(PlayerUID xuid)
{
for (DWORD i = 0; i < s_playerCount; i++)
{
if (Win64_IsActivePlayer(&m_player[i], i) && m_player[i].GetXuid() == xuid) return &m_player[i];
// Conventional XUID implementation except for Windows 64.
if (!Win64_IsActivePlayer(&m_player[i], i))
continue;
if (m_player[i].GetXuid() == xuid)
return &m_player[i];
}
return &m_player[0];
}

View File

@@ -52,6 +52,7 @@
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
#include "..\Minecraft.World\net.minecraft.world.item.h"
#include "..\Minecraft.World\Minecraft.World.h"
#include "Windows64\Windows64_NameXuid.h"
#include "ClientConnection.h"
#include "..\Minecraft.World\HellRandomLevelSource.h"
#include "..\Minecraft.World\net.minecraft.world.entity.animal.h"

View File

@@ -16,6 +16,7 @@
#include "..\Minecraft.World\ArrayWithLength.h"
#include "..\Minecraft.World\net.minecraft.network.packet.h"
#include "..\Minecraft.World\net.minecraft.network.h"
#include "Windows64\Windows64_NameXuid.h"
#include "..\Minecraft.World\Pos.h"
#include "..\Minecraft.World\ProgressListener.h"
#include "..\Minecraft.World\HellRandomLevelSource.h"
@@ -80,13 +81,33 @@ void PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
{
CompoundTag *playerTag = load(player);
INetworkPlayer *networkPlayer = connection->getSocket()->getPlayer();
#ifdef _WINDOWS64 // Attempt fallback save load
if (playerTag == NULL)
{
// The check below should prevent this from happening on a dedicated server (IsHost)
if (networkPlayer != NULL && networkPlayer->IsHost() || networkPlayer == NULL)
{
NetworkPlayerXbox *npx = (NetworkPlayerXbox *)networkPlayer;
IQNetPlayer *qp = npx->GetQNetPlayer();
// Hardcoded as this is a long term legacy failsafe load!
playerTag = playerIo->loadPlayerDataTag((PlayerUID)(0xe000d45248242f2e + qp->m_smallId));
if (playerTag != NULL)
{
player->load(playerTag);
}
}
}
#endif
bool newPlayer = playerTag == NULL;
player->setLevel(server->getLevel(player->dimension));
player->gameMode->setLevel((ServerLevel *)player->level);
// Make sure these privileges are always turned off for the host player
INetworkPlayer *networkPlayer = connection->getSocket()->getPlayer();
if(networkPlayer != NULL && networkPlayer->IsHost())
{
player->enableAllPlayerPrivileges(true);
@@ -518,17 +539,6 @@ shared_ptr<ServerPlayer> PlayerList::getPlayerForLogin(PendingConnection *pendin
player->gameMode->player = player; // 4J added as had to remove this assignment from ServerPlayer ctor
player->setXuid( xuid ); // 4J Added
player->setOnlineXuid( onlineXuid ); // 4J Added
#ifdef _WINDOWS64
{
INetworkPlayer* np = pendingConnection->connection->getSocket()->getPlayer();
if (np != NULL)
{
PlayerUID realXuid = np->GetUID();
player->setXuid(realXuid);
player->setOnlineXuid(realXuid);
}
}
#endif
// Work out the base server player settings
INetworkPlayer *networkPlayer = pendingConnection->connection->getSocket()->getPlayer();
if(networkPlayer != NULL && !networkPlayer->IsHost())

View File

@@ -4,6 +4,7 @@
#include "stdafx.h"
#include <assert.h>
#include <fstream>
#include <iostream>
#include <ShellScalingApi.h>
#include <shellapi.h>
@@ -41,6 +42,7 @@
#include "..\..\Minecraft.World\compression.h"
#include "..\..\Minecraft.World\OldChunkStorage.h"
#include "Network\WinsockNetLayer.h"
#include "Windows64\Windows64_NameXuid.h"
#include "Xbox/resource.h"
@@ -92,6 +94,7 @@ float g_iAspectRatio = static_cast<float>(g_iScreenWidth) / g_iScreenHeight;
char g_Win64Username[17] = { 0 };
wchar_t g_Win64UsernameW[17] = { 0 };
PlayerUID g_Win64PlayerUID = NULL;
// Fullscreen toggle state
static bool g_isFullscreen = false;
@@ -1208,6 +1211,36 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
fclose(f);
}
// Load PlayerUID from file
const char *uidFilePath = "uid.dat";
PlayerUID uid = 0;
// Try to read UID from file if it exists
std::ifstream uidReadableFile(uidFilePath, std::ios::binary);
if (uidReadableFile.is_open())
{
uidReadableFile.read(reinterpret_cast<char *>(&uid), sizeof(PlayerUID));
if (uidReadableFile.gcount() == sizeof(PlayerUID) && uid != (PlayerUID)INVALID_XUID)
{
uidReadableFile.close();
return uid;
}
uidReadableFile.close();
}
// Doesn't exist, so let's write it
uid = Win64NameXuid::GenerateRandomPlayerUID();
std::ofstream uidWriteFile(filePath, std::ios::binary);
if (uidWriteFile.is_open())
{
uidWriteFile.write(reinterpret_cast<const char *>(&uid), sizeof(PlayerUID));
uidWriteFile.close();
}
g_Win64PlayerUID = uid;
// Load stuff from launch options, including username
Win64LaunchOptions launchOptions = ParseLaunchOptions();
ApplyScreenMode(launchOptions.screenMode);

View File

@@ -0,0 +1,65 @@
#pragma once
#ifdef _WINDOWS64
#include <string>
namespace Win64NameXuid
{
/**
* ## Resolves a persistent 64-bit player ID from the player's username.
*
* We keep this deterministic so existing player save/map systems can key off XUID.
*
* @param playerName The player's username.
* @return The resolved PlayerUID.
*/
inline PlayerUID ResolvePersistentXuidFromName(const std::wstring &playerName)
{
const unsigned __int64 fnvOffset = 14695981039346656037ULL;
const unsigned __int64 fnvPrime = 1099511628211ULL;
unsigned __int64 hash = fnvOffset;
for (size_t i = 0; i < playerName.length(); ++i)
{
unsigned short codeUnit = (unsigned short)playerName[i];
hash ^= (unsigned __int64)(codeUnit & 0xFF);
hash *= fnvPrime;
hash ^= (unsigned __int64)((codeUnit >> 8) & 0xFF);
hash *= fnvPrime;
}
// Namespace the hash away from legacy smallId-based values.
hash ^= 0x9E3779B97F4A7C15ULL;
hash |= 0x8000000000000000ULL;
if (hash == (unsigned __int64)INVALID_XUID)
{
hash ^= 0x0100000000000001ULL;
}
return (PlayerUID)hash;
}
/**
* Creates a completely random PlayerUID while also keeping itself away from the legacy values
*
* @return Random PlayerUID within valid ranges
*/
inline PlayerUID GenerateRandomPlayerUID()
{
unsigned __int64 hash = ((unsigned __int64)rand() << 32) | (unsigned __int64)rand();
// Namespace the hash away from legacy smallId-based values.
hash ^= 0x9E3779B97F4A7C15ULL;
hash |= 0x8000000000000000ULL;
if (hash == (unsigned __int64)INVALID_XUID)
{
hash ^= 0x0100000000000001ULL;
}
return (PlayerUID)hash;
}
}
#endif

View File

@@ -220,6 +220,7 @@ public:
bool m_isRemote;
bool m_isHostPlayer;
wchar_t m_gamertag[32];
PlayerUID m_playerUID;
private:
ULONG_PTR m_customData;
};