From 182d76f3916e17727e3dcf9e38db2132360215dd Mon Sep 17 00:00:00 2001 From: kuwa Date: Fri, 6 Mar 2026 15:01:36 +0900 Subject: 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 --- Minecraft.Client/Windows64/Windows64_Minecraft.cpp | 7 + Minecraft.Client/Windows64/Windows64_Xuid.h | 214 +++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 Minecraft.Client/Windows64/Windows64_Xuid.h (limited to 'Minecraft.Client/Windows64') diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index 1146b86d..fa4b8366 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -42,6 +42,7 @@ #include "..\..\Minecraft.World\OldChunkStorage.h" #include "Common/PostProcesser.h" #include "Network\WinsockNetLayer.h" +#include "Windows64_Xuid.h" #include "Xbox/resource.h" @@ -1221,6 +1222,12 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, Win64LaunchOptions launchOptions = ParseLaunchOptions(); ApplyScreenMode(launchOptions.screenMode); + // Ensure uid.dat exists from startup in client mode (before any multiplayer/login path). + if (!launchOptions.serverMode) + { + Win64Xuid::ResolvePersistentXuid(); + } + // If no username, let's fall back if (g_Win64Username[0] == 0) { diff --git a/Minecraft.Client/Windows64/Windows64_Xuid.h b/Minecraft.Client/Windows64/Windows64_Xuid.h new file mode 100644 index 00000000..93577751 --- /dev/null +++ b/Minecraft.Client/Windows64/Windows64_Xuid.h @@ -0,0 +1,214 @@ +#pragma once + +#ifdef _WINDOWS64 + +#include +#include +#include +#include +#include +#include + +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 -- cgit v1.2.3