aboutsummaryrefslogtreecommitdiff
path: root/Minecraft.Client/Windows64
diff options
context:
space:
mode:
authorkuwa <kuwa.com3@gmail.com>2026-03-06 15:01:36 +0900
committerGitHub <noreply@github.com>2026-03-06 00:01:36 -0600
commit182d76f3916e17727e3dcf9e38db2132360215dd (patch)
tree7c9993f0ffe90575ea1568f428ebe25d70f9b77d /Minecraft.Client/Windows64
parentc45541080fc34ce57a9e49f917b8d847634987c4 (diff)
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
Diffstat (limited to 'Minecraft.Client/Windows64')
-rw-r--r--Minecraft.Client/Windows64/Windows64_Minecraft.cpp7
-rw-r--r--Minecraft.Client/Windows64/Windows64_Xuid.h214
2 files changed, 221 insertions, 0 deletions
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 <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