Introduce uid.dat (offline PlayerUIDs), fix multiplayer save data persistence (#536)

* 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).

* fix: preserve legacy host xuid (base xuid + 0) for existing world compatibility

- Add legacy embedded host XUID helper (base + 0).
- When Minecraft.Client is hosting, force only the first host player to use legacy host XUID.
- Keep name-based XUID for non-host players.
- Prevent old singleplayer/hosted worlds from losing/mismatching host player data.

* update: migrate Win64 player uid to `uid.dat`-backed XUID and add XUID based duplicate login guards

- Replace Win64 username-derived XUID resolution with persistent `uid.dat`-backed identity (`Windows64_Xuid` / `Win64Xuid`).
- Persist a per-client XUID next to the executable, with first-run generation, read/write, and process-local caching.
- Keep legacy host compatibility by pinning host self to legacy embedded `base + 0` XUID for existing world/playerdata continuity.
- Propagate packet-authoritative XUIDs into QNet player slots via `m_resolvedXuid`, and use it for `GetXuid`/`GetPlayerByXuid` with legacy fallback.
- Update Win64 profile/network paths to use persistent XUID for non-host clients and clear resolved identity on disconnect.
- Add login-time duplicate checks: reject connections when the same XUID is already connected (in addition to existing duplicate-name checks on Win64).
- Add inline compatibility comments around legacy/new identity coexistence paths for easier future maintenance.

* update: ensure uid.dat exists at startup in client mode for multiplayer
This commit is contained in:
kuwa
2026-03-06 15:01:36 +09:00
committed by GitHub
parent c45541080f
commit 182d76f391
9 changed files with 389 additions and 13 deletions

View File

@@ -759,6 +759,18 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
return;
}
}
#ifdef _WINDOWS64
// Win64 keeps local-player identity separate from network smallId; 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),
@@ -800,22 +812,50 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
#ifdef _WINDOWS64
{
IQNetPlayer* matchedQNetPlayer = NULL;
PlayerUID pktXuid = player->getXuid();
const PlayerUID WIN64_XUID_BASE = (PlayerUID)0xe000d45248242f2e;
// Legacy compatibility path for peers still using embedded smallId XUIDs.
if (pktXuid >= WIN64_XUID_BASE && pktXuid < WIN64_XUID_BASE + MINECRAFT_NET_MAX_PLAYERS)
{
BYTE smallId = (BYTE)(pktXuid - WIN64_XUID_BASE);
INetworkPlayer* np = g_NetworkManager.GetPlayerBySmallId(smallId);
if (np != NULL)
{
NetworkPlayerXbox* npx = (NetworkPlayerXbox*)np;
matchedQNetPlayer = npx->GetQNetPlayer();
}
}
// Current Win64 path: identify QNet player by name and attach packet XUID.
if (matchedQNetPlayer == NULL)
{
for (BYTE smallId = 0; smallId < MINECRAFT_NET_MAX_PLAYERS; ++smallId)
{
INetworkPlayer* np = g_NetworkManager.GetPlayerBySmallId(smallId);
if (np == NULL)
continue;
NetworkPlayerXbox* npx = (NetworkPlayerXbox*)np;
IQNetPlayer* qp = npx->GetQNetPlayer();
if (qp != NULL && qp->m_gamertag[0] == 0)
if (qp != NULL && _wcsicmp(qp->m_gamertag, packet->name.c_str()) == 0)
{
wcsncpy_s(qp->m_gamertag, 32, packet->name.c_str(), _TRUNCATE);
matchedQNetPlayer = qp;
break;
}
}
}
if (matchedQNetPlayer != NULL)
{
// Store packet-authoritative XUID on this network slot so later lookups by XUID
// (e.g. remove player, display mapping) work for both legacy and uid.dat clients.
matchedQNetPlayer->m_resolvedXuid = pktXuid;
if (matchedQNetPlayer->m_gamertag[0] == 0)
{
wcsncpy_s(matchedQNetPlayer->m_gamertag, 32, packet->name.c_str(), _TRUNCATE);
}
}
}
#endif
@@ -985,6 +1025,8 @@ void ClientConnection::handleRemoveEntity(shared_ptr<RemoveEntitiesPacket> packe
qp->m_smallId = 0;
qp->m_isRemote = false;
qp->m_isHostPlayer = false;
// Clear resolved id to avoid stale XUID -> player matches after disconnect.
qp->m_resolvedXuid = INVALID_XUID;
qp->m_gamertag[0] = 0;
qp->SetCustomDataValue(0);
}
@@ -3956,4 +3998,4 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr<
{
m_recievedTick = GetTickCount();
m_packet = packet;
}
}