* 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
215 lines
4.9 KiB
C++
215 lines
4.9 KiB
C++
#pragma once
|
|
|
|
#ifdef _WINDOWS64
|
|
|
|
#include <string>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cerrno>
|
|
#include <cstring>
|
|
#include <Windows.h>
|
|
|
|
namespace Win64Xuid
|
|
{
|
|
inline PlayerUID GetLegacyEmbeddedBaseXuid()
|
|
{
|
|
return (PlayerUID)0xe000d45248242f2eULL;
|
|
}
|
|
|
|
inline PlayerUID GetLegacyEmbeddedHostXuid()
|
|
{
|
|
// Legacy behavior used "embedded base + smallId"; host was always smallId 0.
|
|
// We intentionally keep this value for host/self compatibility with pre-migration worlds.
|
|
return GetLegacyEmbeddedBaseXuid();
|
|
}
|
|
|
|
inline bool IsLegacyEmbeddedRange(PlayerUID xuid)
|
|
{
|
|
// Old Win64 XUIDs were not persistent and always lived in this narrow base+smallId range.
|
|
// Treat them as legacy/non-persistent so uid.dat values never collide with old slot IDs.
|
|
const PlayerUID base = GetLegacyEmbeddedBaseXuid();
|
|
return xuid >= base && xuid < (base + MINECRAFT_NET_MAX_PLAYERS);
|
|
}
|
|
|
|
inline bool IsPersistedUidValid(PlayerUID xuid)
|
|
{
|
|
return xuid != INVALID_XUID && !IsLegacyEmbeddedRange(xuid);
|
|
}
|
|
|
|
|
|
// ./uid.dat
|
|
inline bool BuildUidFilePath(char* outPath, size_t outPathSize)
|
|
{
|
|
if (outPath == NULL || outPathSize == 0)
|
|
return false;
|
|
|
|
outPath[0] = 0;
|
|
|
|
char exePath[MAX_PATH] = {};
|
|
DWORD len = GetModuleFileNameA(NULL, exePath, MAX_PATH);
|
|
if (len == 0 || len >= MAX_PATH)
|
|
return false;
|
|
|
|
char* lastSlash = strrchr(exePath, '\\');
|
|
if (lastSlash != NULL)
|
|
{
|
|
*(lastSlash + 1) = 0;
|
|
}
|
|
|
|
if (strcpy_s(outPath, outPathSize, exePath) != 0)
|
|
return false;
|
|
if (strcat_s(outPath, outPathSize, "uid.dat") != 0)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
inline bool ReadUid(PlayerUID* outXuid)
|
|
{
|
|
if (outXuid == NULL)
|
|
return false;
|
|
|
|
char path[MAX_PATH] = {};
|
|
if (!BuildUidFilePath(path, MAX_PATH))
|
|
return false;
|
|
|
|
FILE* f = NULL;
|
|
if (fopen_s(&f, path, "rb") != 0 || f == NULL)
|
|
return false;
|
|
|
|
char buffer[128] = {};
|
|
size_t readBytes = fread(buffer, 1, sizeof(buffer) - 1, f);
|
|
fclose(f);
|
|
|
|
if (readBytes == 0)
|
|
return false;
|
|
|
|
// Compatibility: earlier experiments may have written raw 8-byte uid.dat.
|
|
if (readBytes == sizeof(unsigned __int64))
|
|
{
|
|
unsigned __int64 raw = 0;
|
|
memcpy(&raw, buffer, sizeof(raw));
|
|
PlayerUID parsed = (PlayerUID)raw;
|
|
if (IsPersistedUidValid(parsed))
|
|
{
|
|
*outXuid = parsed;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
buffer[readBytes] = 0;
|
|
char* begin = buffer;
|
|
while (*begin == ' ' || *begin == '\t' || *begin == '\r' || *begin == '\n')
|
|
{
|
|
++begin;
|
|
}
|
|
|
|
errno = 0;
|
|
char* end = NULL;
|
|
unsigned __int64 raw = _strtoui64(begin, &end, 0);
|
|
if (begin == end || errno != 0)
|
|
return false;
|
|
|
|
while (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')
|
|
{
|
|
++end;
|
|
}
|
|
if (*end != 0)
|
|
return false;
|
|
|
|
PlayerUID parsed = (PlayerUID)raw;
|
|
if (!IsPersistedUidValid(parsed))
|
|
return false;
|
|
|
|
*outXuid = parsed;
|
|
return true;
|
|
}
|
|
|
|
inline bool WriteUid(PlayerUID xuid)
|
|
{
|
|
char path[MAX_PATH] = {};
|
|
if (!BuildUidFilePath(path, MAX_PATH))
|
|
return false;
|
|
|
|
FILE* f = NULL;
|
|
if (fopen_s(&f, path, "wb") != 0 || f == NULL)
|
|
return false;
|
|
|
|
int written = fprintf_s(f, "0x%016llX\n", (unsigned long long)xuid);
|
|
fclose(f);
|
|
return written > 0;
|
|
}
|
|
|
|
inline unsigned __int64 Mix64(unsigned __int64 x)
|
|
{
|
|
x += 0x9E3779B97F4A7C15ULL;
|
|
x = (x ^ (x >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
|
x = (x ^ (x >> 27)) * 0x94D049BB133111EBULL;
|
|
return x ^ (x >> 31);
|
|
}
|
|
|
|
inline PlayerUID GeneratePersistentUid()
|
|
{
|
|
// Avoid rand_s dependency: mix several Win64 runtime values into a 64-bit seed.
|
|
FILETIME ft = {};
|
|
GetSystemTimeAsFileTime(&ft);
|
|
unsigned __int64 t = (((unsigned __int64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
|
|
|
LARGE_INTEGER qpc = {};
|
|
QueryPerformanceCounter(&qpc);
|
|
|
|
unsigned __int64 seed = t;
|
|
seed ^= (unsigned __int64)qpc.QuadPart;
|
|
seed ^= ((unsigned __int64)GetCurrentProcessId() << 32);
|
|
seed ^= (unsigned __int64)GetCurrentThreadId();
|
|
seed ^= (unsigned __int64)GetTickCount();
|
|
seed ^= (unsigned __int64)(size_t)&qpc;
|
|
seed ^= (unsigned __int64)(size_t)GetModuleHandleA(NULL);
|
|
|
|
unsigned __int64 raw = Mix64(seed) ^ Mix64(seed + 0xA0761D6478BD642FULL);
|
|
raw ^= 0x8F4B2D6C1A93E705ULL;
|
|
raw |= 0x8000000000000000ULL;
|
|
|
|
PlayerUID xuid = (PlayerUID)raw;
|
|
if (!IsPersistedUidValid(xuid))
|
|
{
|
|
raw ^= 0x0100000000000001ULL;
|
|
xuid = (PlayerUID)raw;
|
|
}
|
|
|
|
if (!IsPersistedUidValid(xuid))
|
|
{
|
|
// Last-resort deterministic fallback for pathological cases.
|
|
xuid = (PlayerUID)0xD15EA5E000000001ULL;
|
|
}
|
|
|
|
return xuid;
|
|
}
|
|
|
|
inline PlayerUID ResolvePersistentXuid()
|
|
{
|
|
// Process-local cache: uid.dat is immutable during runtime and this path is hot.
|
|
static bool s_cached = false;
|
|
static PlayerUID s_xuid = INVALID_XUID;
|
|
|
|
if (s_cached)
|
|
return s_xuid;
|
|
|
|
PlayerUID fileXuid = INVALID_XUID;
|
|
if (ReadUid(&fileXuid))
|
|
{
|
|
s_xuid = fileXuid;
|
|
s_cached = true;
|
|
return s_xuid;
|
|
}
|
|
|
|
// First launch on this client: generate once and persist to uid.dat.
|
|
s_xuid = GeneratePersistentUid();
|
|
WriteUid(s_xuid);
|
|
s_cached = true;
|
|
return s_xuid;
|
|
}
|
|
}
|
|
|
|
#endif
|