Add Mojang/Ely.by/offline authentication system

The old Windows64 port had no real player identity — it used hardcoded
fake XUIDs, so anyone could impersonate anyone. This replaces that with
proper auth supporting Mojang, Ely.by, and offline accounts.

MCAuth library (new, MCAuth/):
  Mojang auth via MSA device code flow (XBL, SISU, MC services),
  Ely.by via Yggdrasil with 2FA, offline UUID generation matching
  Java Edition (MD5 v3 from "OfflinePlayer:<name>"). Multi-account
  manager with background token refresh, per-slot sessions, and
  on-disk token persistence. Server-side session verification via
  Mojang/Ely.by hasJoined API. Skin fetching and PNG validation
  from texture servers.

Network protocol (version bumped to 80):
  Three new packets (AuthScheme, AuthResponse, AuthResult) implement
  a server-driven auth handshake before login completes. Player
  identity migrated from 64-bit XUID to 128-bit GameUUID backed by
  two uint64 fields (hi/lo). readPlayerUID/writePlayerUID now
  serialize 16 bytes on the wire. Old and new clients cannot connect
  to each other — version mismatch is rejected at PreLogin.

Save migration:
  Map data mappings auto-migrate from old format: the old 64-bit
  XUID is placed in hi, lo is set to 0 as a sentinel. On first
  access by the real player, the sentinel entry is upgraded in-place
  to the full 128-bit UUID. Format detection is by file size (2080,
  2112, or 4160 bytes). Player .dat filenames inside saveData.ms
  change from decimal XUID to dashed UUID — old saves need manual
  entry renaming in the archive.

UI:
  NativeUIRenderer: immediate-mode drawing system (quads, text,
  9-slice panels, scrollbars, focus lists) for rendering auth
  screens without Flash/Scaleform. UIScene_MSAuth handles device
  code display, Ely.by credential input with 2FA, per-account
  skin head preview, and multi-account add/remove/switch.

Server:
  online-mode and auth-provider (mojang/elyby) in server.properties.
  Whitelist and ban checks validate against the server-verified UUID.
  Incompatible auth scheme logs which provider the server expects
  vs what the client is using.

Also fixes a pre-existing exploit where any client could send a
DebugOptionsPacket to grant themselves CraftAnything and other debug
privileges on any server — now requires OP status server-side.
This commit is contained in:
MrTheShy 2026-03-23 01:14:23 +01:00
parent a94ee1ca22
commit 4f2352361a
123 changed files with 11017 additions and 1026 deletions

View file

@ -76,6 +76,7 @@ list(APPEND MINECRAFT_SHARED_DEFINES ${PLATFORM_DEFINES})
# --- # ---
# Sources # Sources
# --- # ---
add_subdirectory(MCAuth)
add_subdirectory(Minecraft.World) add_subdirectory(Minecraft.World)
add_subdirectory(Minecraft.Client) add_subdirectory(Minecraft.Client)
if(PLATFORM_NAME STREQUAL "Windows64") # Server is only supported on Windows for now if(PLATFORM_NAME STREQUAL "Windows64") # Server is only supported on Windows for now
@ -111,3 +112,4 @@ set_property(DIRECTORY PROPERTY VS_STARTUP_PROJECT Minecraft.Client)
# Setup folders for Visual Studio, just hides the build targets under a sub folder # Setup folders for Visual Studio, just hides the build targets under a sub folder
set_property(GLOBAL PROPERTY USE_FOLDERS ON) set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set_property(TARGET GenerateBuildVer PROPERTY FOLDER "Build") set_property(TARGET GenerateBuildVer PROPERTY FOLDER "Build")
set_property(TARGET MCAuth PROPERTY FOLDER "Libraries")

40
MCAuth/CMakeLists.txt Normal file
View file

@ -0,0 +1,40 @@
set(MCAUTH_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthCrypto.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthCrypto.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthHttp.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthHttp.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthInternal.h"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthJava.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthManager.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthElyby.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/MCAuthSession.cpp"
)
source_group("src" FILES ${MCAUTH_SOURCES})
set(MCAUTH_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/MCAuth.h"
"${CMAKE_CURRENT_SOURCE_DIR}/include/MCAuthManager.h"
)
source_group("include" FILES ${MCAUTH_HEADERS})
add_library(MCAuth STATIC ${MCAUTH_SOURCES} ${MCAUTH_HEADERS})
target_include_directories(MCAuth
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/include"
PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
)
target_compile_definitions(MCAuth PRIVATE
_LIB
$<$<CONFIG:Debug>:_DEBUG>
_CRT_SECURE_NO_WARNINGS
)
configure_compiler_target(MCAuth)
target_link_libraries(MCAuth PRIVATE
winhttp
bcrypt
)

156
MCAuth/MCAuth.vcxproj Normal file
View file

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>MCAuth</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>StaticLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)bin\$(Configuration)\$(Platform)\</OutDir>
<IntDir>$(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>Disabled</Optimization>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)MCAuth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)MCAuth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>Disabled</Optimization>
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)MCAuth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Lib>
<OutputFile>$(OutDir)MCAuth.lib</OutputFile>
</Lib>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="include\MCAuth.h" />
<ClInclude Include="include\MCAuthManager.h" />
<ClInclude Include="src\MCAuthCrypto.h" />
<ClInclude Include="src\MCAuthHttp.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\MCAuthCrypto.cpp" />
<ClCompile Include="src\MCAuthHttp.cpp" />
<ClCompile Include="src\MCAuth.cpp" />
<ClCompile Include="src\MCAuthJava.cpp" />
<ClCompile Include="src\MCAuthManager.cpp" />
<ClCompile Include="src\MCAuthSession.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

230
MCAuth/include/MCAuth.h Normal file
View file

@ -0,0 +1,230 @@
#pragma once
/*
* MCAuth - Microsoft/Xbox Live Authentication library for Minecraft Java Edition
* C++ port of the MinecraftAuth Java library by RaphiMC
*
* Usage:
* MCAuth::JavaAuthManager auth;
* MCAuth::JavaSession session;
* std::string error;
*
* bool ok = auth.Login(
* [](const MCAuth::DeviceCodeInfo& dc) {
* // Show dc.userCode and dc.directUri to the user
* },
* session, error
* );
*/
#include <string>
#include <functional>
#include <cstdint>
#include <memory>
#include <vector>
namespace MCAuth {
// ----- Error codes -----
enum class AuthErrorCode {
None = 0,
NetworkTimeout, // connection timed out or DNS failure
NetworkError, // general HTTP/TLS failure (send/receive)
HttpError, // unexpected HTTP status code
InvalidCredentials, // wrong token, expired refresh token, account not authorised
TokenExpired, // access token expired (needs refresh)
ServerUnavailable, // Mojang/MS service returned 5xx
RateLimited, // HTTP 429
ProfileNotOwned, // MS account exists but doesn't own MC Java Edition
Cancelled, // operation was cancelled (e.g. device code timeout)
InternalError, // unexpected exception / crypto failure
};
struct AuthError {
AuthErrorCode code = AuthErrorCode::None;
std::string message;
int httpStatus = 0; // 0 if not applicable
bool ok() const { return code == AuthErrorCode::None; }
explicit operator bool() const { return !ok(); } // true means error
};
// ----- Constants ----
// Java Edition title ID (Win32, used for SISU auth flow)
static constexpr const char* JAVA_CLIENT_ID = "00000000402b5328";
static constexpr const char* MSA_SCOPE = "service::user.auth.xboxlive.com::MBI_SSL";
// ----- Public data types -----
struct DeviceCodeInfo {
std::string userCode; // e.g. "ABCD1234"
std::string verificationUri; // e.g. "https://www.microsoft.com/link"
std::string directUri; // verificationUri + "?otc=" + userCode
std::string deviceCode; // internal polling token
int64_t expiresMs; // absolute timestamp (ms since epoch)
int64_t intervalMs; // polling interval in ms
};
using DeviceCodeCallback = std::function<void(const DeviceCodeInfo&)>;
struct JavaSession {
std::string username; // Minecraft username (from /minecraft/profile)
std::string uuid; // Player UUID (dashed, e.g. "069a79f4-44e9-4726-a5be-fca90e38aaf5")
std::string accessToken; // Full Authorization header value ("Bearer <token>")
int64_t expireMs; // When the access token expires (ms since epoch)
};
// ----- Java Edition auth manager -----
class JavaAuthManager {
public:
JavaAuthManager();
~JavaAuthManager();
JavaAuthManager(const JavaAuthManager&) = delete;
JavaAuthManager& operator=(const JavaAuthManager&) = delete;
// Synchronous device-code login (blocks until complete or timeout).
bool Login(DeviceCodeCallback onDeviceCode,
JavaSession& outSession,
std::string& error,
int timeoutSeconds = 300);
// Refresh using the saved MSA refresh token.
bool Refresh(JavaSession& outSession, std::string& error);
bool IsLoggedIn() const;
void Logout();
// Signal a blocking Login() to abort as soon as possible.
// Thread-safe — can be called from any thread while Login() is running.
void RequestCancel();
bool SaveTokens(const std::string& filePath) const;
bool LoadTokens(const std::string& filePath);
private:
struct Impl;
std::unique_ptr<Impl> m_impl;
};
// ----- Ely.by Yggdrasil auth (free functions, no class) -----
struct ElybyTokens {
std::string accessToken;
std::string clientToken;
std::string uuid; // undashed
std::string username;
};
// POST https://authserver.ely.by/auth/authenticate
// On 2FA requirement, error is set to "elyby_2fa_required"; caller retries with password = "pass:totp_code"
bool ElybyLogin(const std::string& username, const std::string& password,
ElybyTokens& outTokens, std::string& error);
// POST https://authserver.ely.by/auth/refresh
bool ElybyRefresh(ElybyTokens& tokens, std::string& error);
// POST https://authserver.ely.by/auth/validate
bool ElybyValidate(const std::string& accessToken, std::string& error);
// Token persistence — simple JSON: {accessToken, clientToken, uuid, username}
bool ElybyLoadTokens(const std::string& path, ElybyTokens& out);
bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens);
// ----- Skin key utility -----
// Build the canonical memory-texture key for a Mojang skin.
// uuid can be dashed or undashed — dashes are stripped automatically.
// Returns e.g. "mojang_skin_069a79f444e94726a5befca90e38aaf5.png"
std::string MakeSkinKey(const std::string& uuid);
// ----- UUID utilities -----
// Remove dashes from a UUID string: "069a79f4-44e9-..." → "069a79f444e9..."
std::string UndashUuid(const std::string& dashed);
// Insert dashes into a 32-char hex UUID: "069a79f444e9..." → "069a79f4-44e9-..."
std::string DashUuid(const std::string& undashed);
// Generate a Minecraft-compatible offline UUID (UUID v3, namespace "OfflinePlayer:" + username).
// Returns dashed format.
std::string GenerateOfflineUuid(const std::string& username);
// Parse a dashed UUID into two 64-bit halves (big-endian).
struct Uuid128 {
uint64_t hi = 0;
uint64_t lo = 0;
bool isValid() const { return hi != 0 || lo != 0; }
};
Uuid128 ParseUuid128(const std::string& dashed);
// ----- Session verification (Mojang sessionserver) -----
// Client-side: call BEFORE connecting to an online-mode server.
// Posts to sessionserver.mojang.com/session/minecraft/join.
// accessToken = raw token (without "Bearer " prefix).
// undashedUuid = 32-char hex UUID of the player.
// serverId = the server's challenge string.
// Returns true on success (HTTP 204).
bool JoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error);
// Server-side result from HasJoined verification.
struct HasJoinedResult {
bool success = false;
AuthError error; // structured error (code + message + httpStatus)
std::string username; // verified username from Mojang
std::string uuid; // undashed UUID
std::string skinUrl; // Mojang skin texture URL (from base64 properties)
std::string capeUrl; // Mojang cape texture URL (from base64 properties)
};
// Server-side: verify that a player has called /join.
// GETs sessionserver.mojang.com/session/minecraft/hasJoined?username=X&serverId=Y.
// Returns success=true with username+uuid on HTTP 200, success=false on 204 (not joined).
HasJoinedResult HasJoined(const std::string& username,
const std::string& serverId,
std::string& error);
// Download a skin/cape PNG from a URL.
// Returns the raw PNG bytes, or empty vector on failure.
std::vector<uint8_t> FetchSkinPng(const std::string& url, std::string& error);
// Fetch the skin URL for a player UUID from Mojang's session server.
// uuid can be dashed or undashed.
// Returns the skin texture URL, or empty string on failure.
std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error);
// Download a skin PNG and return the raw bytes WITHOUT cropping to 64x32.
// Use this when you need the full 64x64 texture (e.g. for rendering the head).
std::vector<uint8_t> FetchSkinPngRaw(const std::string& url, std::string& error);
// ----- Ely.by session functions (same Yggdrasil protocol, different URLs) -----
bool ElybyJoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error);
HasJoinedResult ElybyHasJoined(const std::string& username,
const std::string& serverId,
std::string& error);
std::string ElybyFetchProfileSkinUrl(const std::string& uuid, std::string& error);
std::string MakeElybySkinKey(const std::string& uuid);
// ----- Skin validation -----
// Maximum allowed skin PNG size in bytes (32KB — a 64x64 RGBA PNG is ~4KB compressed)
static constexpr size_t kMaxSkinBytes = 32768;
// Check PNG magic, size cap, and 64x32 or 64x64 dimensions.
bool ValidateSkinPng(const uint8_t* data, size_t size);
} // namespace MCAuth

View file

@ -0,0 +1,211 @@
#pragma once
/*
* MCAuthManager - Thread-safe singleton that wraps JavaAuthManager with
* multi-account and per-slot session support (for splitscreen).
*
* Architecture:
*
* - Account list (m_javaAccounts) is SHARED one global list of saved accounts.
* - Each player slot (0..XUSER_MAX_COUNT-1) has its own AuthSlot containing:
* - A shared_ptr<JavaAuthManager> (ownership shared with background worker)
* - Its own session, mutex, generation counter
* - Slot 0 is the primary player. Slots 1-3 are splitscreen.
*
* Thread model (fire-and-forget):
* - Background workers are detached threads that capture a shared_ptr copy
* of the auth engine they operate on.
* - To cancel: increment generation + RequestCancel on the old auth engine,
* then create a fresh shared_ptr<JavaAuthManager> for the slot.
* - The old thread runs to completion harmlessly generation mismatch causes
* it to discard results, and the old auth engine is freed when the thread
* exits (shared_ptr ref-count drops to zero).
* - The main thread NEVER calls join(). No blocking. Ever.
*
* Usage:
* auto& mgr = MCAuthManager::Get();
* mgr.LoadJavaAccountIndex();
* mgr.TryRestoreActiveJavaAccount(); // restore slot 0
*
* mgr.SetAccountForSlot(0, accountIndex); // switch slot 0's account
* auto session = mgr.GetSlotSession(0); // get slot 0's session
*/
#include "MCAuth.h"
#include <functional>
#include <mutex>
#include <atomic>
#include <string>
#include <vector>
#include <memory>
#include <condition_variable>
#ifndef XUSER_MAX_COUNT
#define XUSER_MAX_COUNT 4
#endif
class MCAuthManager {
public:
enum class State {
Idle,
WaitingForCode, // device code displayed, waiting for user
Authenticating, // polling / running auth chain
Success,
Failed,
};
// Per-account info stored in the index.
struct JavaAccountInfo {
std::string username; // Minecraft username
std::string uuid; // Player UUID (dashed)
std::string tokenFile; // e.g. "java_auth_0.json" (empty for offline)
bool isOffline = false;
std::string authProvider; // "mojang", "elyby", "offline" (default: "mojang")
};
// Per-player-slot auth state. Each slot has its own auth engine + session.
struct AuthSlot {
// Auth engine is heap-allocated and shared with background worker threads.
// When we cancel, we abandon the old shared_ptr and create a fresh one.
// The old thread's copy prevents use-after-free; it finishes harmlessly.
std::shared_ptr<MCAuth::JavaAuthManager> auth;
MCAuth::ElybyTokens elybyTokens; // Ely.by tokens (no engine class needed)
MCAuth::JavaSession session; // current session data
std::atomic<int> accountIndex{-1}; // index into m_javaAccounts (-1 = none)
mutable std::mutex mutex;
std::atomic<uint32_t> generation{0}; // invalidates stale workers
std::atomic<State> state{State::Idle};
mutable std::condition_variable cv; // signals when state changes
std::string lastError;
// No std::thread member — workers are detached (fire-and-forget).
bool hasSession() const { return !session.uuid.empty(); }
AuthSlot() : auth(std::make_shared<MCAuth::JavaAuthManager>()) {}
};
using DeviceCodeCb = MCAuth::DeviceCodeCallback;
using JavaCompleteCb = std::function<void(bool, const MCAuth::JavaSession&, const std::string&)>;
static MCAuthManager& Get();
MCAuthManager(const MCAuthManager&) = delete;
MCAuthManager& operator=(const MCAuthManager&) = delete;
// ---- Java Edition — Account Index ----
bool LoadJavaAccountIndex();
bool SaveJavaAccountIndex() const;
std::vector<JavaAccountInfo> GetJavaAccounts() const;
// ---- Java Edition — Per-Slot Session Management ----
// Get the auth slot for a player index (0 = primary, 1-3 = splitscreen).
const AuthSlot& GetSlot(int slot) const;
// Set which account a slot uses. Loads tokens + refreshes in background.
// The old session remains valid until the new one is ready (no empty window).
bool SetAccountForSlot(int slot, int accountIndex);
// Clear a slot (e.g. when a splitscreen player leaves).
void ClearSlot(int slot);
// Get the session for a specific slot.
MCAuth::JavaSession GetSlotSession(int slot) const;
// Check if a slot has a valid logged-in session.
bool IsSlotLoggedIn(int slot) const;
// Check if the slot's token is expiring soon (within 60s).
bool IsTokenExpiringSoon(int slot) const;
// Proactively refresh a slot's token in the background.
void RefreshSlot(int slot);
// Block until a slot's auth settles (state leaves Authenticating/WaitingForCode).
// Returns true if login succeeded, false on timeout or failure.
bool WaitForSlotReady(int slot, int timeoutMs = 15000) const;
// Check if an online account is already assigned to another slot.
bool IsAccountInUseByOtherSlot(int slot, int accountIndex) const;
// ---- Java Edition — Slot 0 Convenience (backward compat) ----
int GetActiveJavaAccountIndex() const;
bool SetActiveJavaAccount(int index) { return SetAccountForSlot(0, index); }
MCAuth::JavaSession GetJavaSession() const;
bool IsJavaLoggedIn() const;
State GetState() const { return m_slots[0].state.load(); }
State GetSlotState(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
return m_slots[slot].state.load();
}
bool WaitForAuthReady(int timeoutMs = 15000) const { return WaitForSlotReady(0, timeoutMs); }
// ---- Java Edition — Account Management ----
// Add a new account via device code flow (background thread on slot 0).
void BeginAddJavaAccount(DeviceCodeCb onDeviceCode, JavaCompleteCb onComplete,
int timeoutSeconds = 300);
// Add an Ely.by account via username+password.
using ElybyCompleteCb = std::function<void(bool ok, const MCAuth::JavaSession& session, const std::string& error)>;
using Elyby2FACb = std::function<void()>;
void BeginAddElybyAccount(const std::string& username, const std::string& password,
ElybyCompleteCb onComplete, Elyby2FACb on2FA = nullptr);
// Add an offline account. Returns new account index or -1.
int AddOfflineJavaAccount(const std::string& username);
// Remove an account by index. Deletes its token file.
bool RemoveJavaAccount(int index);
// Restore the active account from disk at startup (slot 0).
void TryRestoreActiveJavaAccount();
// ---- Device code info (valid during add-account flow) ----
std::string GetJavaDeviceCode() const;
std::string GetJavaDirectUri() const;
std::string GetLastError() const;
// File paths
static constexpr const char* kJavaAccountsFile = "java_accounts.json";
private:
MCAuthManager();
~MCAuthManager();
// Generate a unique token filename for a new account.
std::string AllocTokenFile(const std::string& uuid) const;
// Synthesize an offline session from account info.
static MCAuth::JavaSession SynthesizeOfflineSession(const JavaAccountInfo& acct);
// Abandon the current auth engine for a slot and create a fresh one.
// Calls RequestCancel on the old engine so its worker exits promptly.
// Returns the fresh shared_ptr for capturing in the new worker lambda.
std::shared_ptr<MCAuth::JavaAuthManager> ResetSlotAuth(AuthSlot& s);
// Shared Ely.by refresh logic used by RefreshSlot and SetAccountForSlot.
// If failOpen is true, sets state to Success even when refresh fails (fail-open).
// If alwaysSaveIndex is true, calls SaveJavaAccountIndex even on failure.
void RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile,
bool failOpen, bool alwaysSaveIndex);
// Per-slot auth state
AuthSlot m_slots[XUSER_MAX_COUNT];
// Shared account list (protected by m_accountsMutex)
mutable std::mutex m_accountsMutex;
std::vector<JavaAccountInfo> m_javaAccounts;
// Device code state (for BeginAddJavaAccount, always on slot 0)
mutable std::mutex m_deviceCodeMutex;
std::string m_javaDeviceCode;
std::string m_javaDirectUri;
bool m_javaRestoreAttempted = false;
};

398
MCAuth/src/MCAuthCrypto.cpp Normal file
View file

@ -0,0 +1,398 @@
#include "MCAuthCrypto.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
#include <stdexcept>
#include <chrono>
#include <cassert>
#include <cstring>
#include <cstdio>
namespace MCAuth {
static void ThrowIfFailed(NTSTATUS status, const char* ctx) {
if (!BCRYPT_SUCCESS(status)) {
char buf[128];
snprintf(buf, sizeof(buf), "BCrypt error 0x%08X in %s", (unsigned)status, ctx);
throw std::runtime_error(buf);
}
}
// Build SubjectPublicKeyInfo DER for an EC public key.
// Hardcoded for P-256 and P-384 (the only two we use).
std::vector<uint8_t> BuildSubjectPublicKeyInfoDER(
int bitSize,
const std::vector<uint8_t>& x,
const std::vector<uint8_t>& y)
{
// OIDs
// ecPublicKey: 1.2.840.10045.2.1 -> 2a 86 48 ce 3d 02 01 (7 bytes)
static const uint8_t OID_EC_PUBLIC_KEY[] = { 0x2a,0x86,0x48,0xce,0x3d,0x02,0x01 };
// secp256r1 : 1.2.840.10045.3.1.7 -> 2a 86 48 ce 3d 03 01 07 (8 bytes)
static const uint8_t OID_P256[] = { 0x2a,0x86,0x48,0xce,0x3d,0x03,0x01,0x07 };
// secp384r1 : 1.3.132.0.34 -> 2b 81 04 00 22 (5 bytes)
static const uint8_t OID_P384[] = { 0x2b,0x81,0x04,0x00,0x22 };
size_t coordSize = (bitSize == 256) ? 32 : 48;
const uint8_t* curveOid = (bitSize == 256) ? OID_P256 : OID_P384;
size_t curveOidLen = (bitSize == 256) ? 8 : 5;
// AlgorithmIdentifier SEQUENCE:
// 06 07 <ecPublicKey OID>
// 06 <len> <curve OID>
size_t algIdInnerLen = 2 + sizeof(OID_EC_PUBLIC_KEY) + 2 + curveOidLen;
// BIT STRING content: 00 04 X Y
size_t bitStringContent = 1 + 1 + coordSize + coordSize; // 00 04 X Y
size_t bitStringLen = bitStringContent;
// Outer SEQUENCE content
size_t outerContent = (2 + algIdInnerLen) + (2 + bitStringLen);
// Single-byte DER lengths suffice for P-256 (89 bytes) and P-384 (118 bytes)
std::vector<uint8_t> der;
auto appendLen = [&](size_t len) {
if (len < 128) {
der.push_back((uint8_t)len);
} else {
der.push_back(0x82);
der.push_back((uint8_t)(len >> 8));
der.push_back((uint8_t)(len & 0xFF));
}
};
// Outer SEQUENCE
der.push_back(0x30);
appendLen(outerContent);
// AlgorithmIdentifier SEQUENCE
der.push_back(0x30);
appendLen(algIdInnerLen);
der.push_back(0x06); der.push_back((uint8_t)sizeof(OID_EC_PUBLIC_KEY));
der.insert(der.end(), OID_EC_PUBLIC_KEY, OID_EC_PUBLIC_KEY + sizeof(OID_EC_PUBLIC_KEY));
der.push_back(0x06); der.push_back((uint8_t)curveOidLen);
der.insert(der.end(), curveOid, curveOid + curveOidLen);
// BIT STRING
der.push_back(0x03);
appendLen(bitStringLen + 1); // +1 for the "unused bits" byte
der.push_back(0x00); // 0 unused bits
der.push_back(0x04); // uncompressed point
der.insert(der.end(), x.begin(), x.end());
der.insert(der.end(), y.begin(), y.end());
return der;
}
// Generate a key pair using BCrypt.
static ECKeyPair GenerateKeyPair(int bitSize) {
LPCWSTR algId = (bitSize == 256) ? BCRYPT_ECDSA_P256_ALGORITHM
: BCRYPT_ECDSA_P384_ALGORITHM;
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_KEY_HANDLE hKey = nullptr;
try {
ThrowIfFailed(BCryptOpenAlgorithmProvider(&hAlg, algId, nullptr, 0), "OpenAlgProvider");
ThrowIfFailed(BCryptGenerateKeyPair(hAlg, &hKey, bitSize, 0), "GenerateKeyPair");
ThrowIfFailed(BCryptFinalizeKeyPair(hKey, 0), "FinalizeKeyPair");
// Export private blob
ULONG privBlobLen = 0;
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPRIVATE_BLOB, nullptr, 0, &privBlobLen, 0), "ExportPriv(len)");
std::vector<uint8_t> privBlob(privBlobLen);
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPRIVATE_BLOB, privBlob.data(), privBlobLen, &privBlobLen, 0), "ExportPriv");
// Export public blob
ULONG pubBlobLen = 0;
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPUBLIC_BLOB, nullptr, 0, &pubBlobLen, 0), "ExportPub(len)");
std::vector<uint8_t> pubBlob(pubBlobLen);
ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPUBLIC_BLOB, pubBlob.data(), pubBlobLen, &pubBlobLen, 0), "ExportPub");
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
// BCRYPT_ECCKEY_BLOB header: 4-byte Magic + 4-byte cbKey
// Public blob: [8-byte header] [X:cbKey bytes] [Y:cbKey bytes]
const BCRYPT_ECCKEY_BLOB* hdr = reinterpret_cast<const BCRYPT_ECCKEY_BLOB*>(pubBlob.data());
ULONG cbKey = hdr->cbKey;
std::vector<uint8_t> x(pubBlob.begin() + 8, pubBlob.begin() + 8 + cbKey);
std::vector<uint8_t> y(pubBlob.begin() + 8 + cbKey, pubBlob.begin() + 8 + cbKey * 2);
ECKeyPair kp;
kp.bitSize = bitSize;
kp.privateBlob = std::move(privBlob);
kp.publicBlob = std::move(pubBlob);
kp.x = x;
kp.y = y;
kp.publicKeyDER = BuildSubjectPublicKeyInfoDER(bitSize, x, y);
return kp;
} catch (...) {
if (hKey) BCryptDestroyKey(hKey);
if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
throw;
}
}
ECKeyPair GenerateP256KeyPair() { return GenerateKeyPair(256); }
ECKeyPair GenerateP384KeyPair() { return GenerateKeyPair(384); }
std::vector<uint8_t> SignSHA256P256(const ECKeyPair& kp,
const uint8_t* data,
size_t len)
{
if (kp.bitSize != 256)
throw std::runtime_error("SignSHA256P256: key is not P-256");
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_KEY_HANDLE hKey = nullptr;
BCRYPT_ALG_HANDLE hHashAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
try {
ThrowIfFailed(BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_ECDSA_P256_ALGORITHM, nullptr, 0), "Sign/OpenAlg");
// Import private key
ThrowIfFailed(BCryptImportKeyPair(hAlg, nullptr, BCRYPT_ECCPRIVATE_BLOB,
&hKey,
const_cast<uint8_t*>(kp.privateBlob.data()),
(ULONG)kp.privateBlob.size(), 0), "Sign/ImportKey");
// Hash the data with SHA-256
ThrowIfFailed(BCryptOpenAlgorithmProvider(&hHashAlg, BCRYPT_SHA256_ALGORITHM, nullptr, 0), "Sign/OpenHashAlg");
ThrowIfFailed(BCryptCreateHash(hHashAlg, &hHash, nullptr, 0, nullptr, 0, 0), "Sign/CreateHash");
ThrowIfFailed(BCryptHashData(hHash, const_cast<uint8_t*>(data), (ULONG)len, 0), "Sign/HashData");
uint8_t digest[32];
ThrowIfFailed(BCryptFinishHash(hHash, digest, sizeof(digest), 0), "Sign/FinishHash");
BCryptDestroyHash(hHash); hHash = nullptr;
BCryptCloseAlgorithmProvider(hHashAlg, 0); hHashAlg = nullptr;
// Sign the hash
ULONG sigLen = 0;
ThrowIfFailed(BCryptSignHash(hKey, nullptr, digest, sizeof(digest), nullptr, 0, &sigLen, 0), "Sign/GetSigLen");
std::vector<uint8_t> sig(sigLen);
ThrowIfFailed(BCryptSignHash(hKey, nullptr, digest, sizeof(digest), sig.data(), sigLen, &sigLen, 0), "Sign/SignHash");
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
sig.resize(sigLen);
return sig;
} catch (...) {
if (hHash) BCryptDestroyHash(hHash);
if (hHashAlg) BCryptCloseAlgorithmProvider(hHashAlg, 0);
if (hKey) BCryptDestroyKey(hKey);
if (hAlg) BCryptCloseAlgorithmProvider(hAlg, 0);
throw;
}
}
static const char kB64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
static const char kB64UrlChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
static std::string Base64EncodeInternal(const uint8_t* data, size_t len, const char* alpha, bool pad) {
std::string out;
out.reserve(((len + 2) / 3) * 4);
for (size_t i = 0; i < len; i += 3) {
uint32_t v = (uint32_t)data[i] << 16;
if (i + 1 < len) v |= (uint32_t)data[i + 1] << 8;
if (i + 2 < len) v |= (uint32_t)data[i + 2];
out += alpha[(v >> 18) & 0x3F];
out += alpha[(v >> 12) & 0x3F];
out += (i + 1 < len) ? alpha[(v >> 6) & 0x3F] : (pad ? '=' : '\0');
out += (i + 2 < len) ? alpha[(v >> 0) & 0x3F] : (pad ? '=' : '\0');
}
// Remove trailing null chars (only happens for url encode without pad)
while (!out.empty() && out.back() == '\0') out.pop_back();
return out;
}
std::string Base64Encode(const uint8_t* data, size_t len) {
return Base64EncodeInternal(data, len, kB64Chars, true);
}
std::string Base64Encode(const std::vector<uint8_t>& data) {
return Base64Encode(data.data(), data.size());
}
std::string Base64UrlEncode(const uint8_t* data, size_t len) {
return Base64EncodeInternal(data, len, kB64UrlChars, false);
}
std::string Base64UrlEncode(const std::vector<uint8_t>& data) {
return Base64UrlEncode(data.data(), data.size());
}
std::string BuildProofKeyJson(const ECKeyPair& p256) {
std::string xEnc = Base64UrlEncode(p256.x);
std::string yEnc = Base64UrlEncode(p256.y);
return "{\"kty\":\"EC\",\"alg\":\"ES256\",\"crv\":\"P-256\",\"use\":\"sig\","
"\"x\":\"" + xEnc + "\",\"y\":\"" + yEnc + "\"}";
}
// XBL Signature header
// Payload to sign (all fields big-endian where integer):
// INT32 policy_version = 1
// BYTE 0x00
// INT64 windows_timestamp
// BYTE 0x00
// BYTES method (e.g. "POST")
// BYTE 0x00
// BYTES url_path_and_query
// BYTE 0x00
// BYTES authorization_header (empty string if none)
// BYTE 0x00
// BYTES request_body
// BYTE 0x00
//
// Signature header value (base64 of):
// INT32 policy_version = 1
// INT64 windows_timestamp
// BYTES 64-byte P1363 ECDSA-SHA256 signature
static void AppendBE32(std::vector<uint8_t>& buf, uint32_t v) {
buf.push_back((uint8_t)(v >> 24));
buf.push_back((uint8_t)(v >> 16));
buf.push_back((uint8_t)(v >> 8));
buf.push_back((uint8_t)(v ));
}
static void AppendBE64(std::vector<uint8_t>& buf, uint64_t v) {
buf.push_back((uint8_t)(v >> 56));
buf.push_back((uint8_t)(v >> 48));
buf.push_back((uint8_t)(v >> 40));
buf.push_back((uint8_t)(v >> 32));
buf.push_back((uint8_t)(v >> 24));
buf.push_back((uint8_t)(v >> 16));
buf.push_back((uint8_t)(v >> 8));
buf.push_back((uint8_t)(v ));
}
std::string BuildXblSignatureHeader(const ECKeyPair& p256,
const std::string& method,
const std::string& urlPath,
const std::string& body,
const std::string& authHdr)
{
// Windows timestamp: (unix_seconds + 11644473600) * 10_000_000
using namespace std::chrono;
int64_t epochSec = duration_cast<seconds>(
system_clock::now().time_since_epoch()).count();
uint64_t winTs = (uint64_t)(epochSec + 11644473600LL) * 10000000ULL;
// Build the content to sign
std::vector<uint8_t> content;
content.reserve(512);
AppendBE32(content, 1); // policy version
content.push_back(0x00);
AppendBE64(content, winTs); // timestamp
content.push_back(0x00);
content.insert(content.end(), method.begin(), method.end());
content.push_back(0x00);
content.insert(content.end(), urlPath.begin(), urlPath.end());
content.push_back(0x00);
content.insert(content.end(), authHdr.begin(), authHdr.end());
content.push_back(0x00);
content.insert(content.end(), body.begin(), body.end());
content.push_back(0x00);
// Sign
std::vector<uint8_t> sig = SignSHA256P256(p256, content.data(), content.size());
// Build header blob
std::vector<uint8_t> hdrBlob;
hdrBlob.reserve(4 + 8 + sig.size());
AppendBE32(hdrBlob, 1);
AppendBE64(hdrBlob, winTs);
hdrBlob.insert(hdrBlob.end(), sig.begin(), sig.end());
return Base64Encode(hdrBlob);
}
std::string GenerateUUID() {
uint8_t bytes[16];
NTSTATUS st = BCryptGenRandom(nullptr, bytes, sizeof(bytes), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
if (!BCRYPT_SUCCESS(st)) {
// fallback to rand (not cryptographic, but OK for device ID purposes)
for (auto& b : bytes) b = (uint8_t)(rand() & 0xFF);
}
// Set version 4 and variant bits
bytes[6] = (bytes[6] & 0x0F) | 0x40;
bytes[8] = (bytes[8] & 0x3F) | 0x80;
char buf[37];
snprintf(buf, sizeof(buf),
"%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5],
bytes[6], bytes[7],
bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]);
return buf;
}
std::vector<uint8_t> Base64Decode(const std::string& s) {
static const int kInv[256] = {
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63,
52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,
15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1,
-1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,
41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
};
std::vector<uint8_t> out;
int buf = 0, bits = 0;
for (unsigned char c : s) {
if (c == '=' || kInv[c] < 0) continue;
buf = (buf << 6) | kInv[c];
bits += 6;
if (bits >= 8) {
bits -= 8;
out.push_back((uint8_t)(buf >> bits));
buf &= (1 << bits) - 1;
}
}
return out;
}
std::string Base64DecodeStr(const std::string& encoded) {
auto bytes = Base64Decode(encoded);
return std::string(bytes.begin(), bytes.end());
}
std::vector<uint8_t> ComputeMD5(const void* data, size_t len) {
std::vector<uint8_t> result(16, 0);
BCRYPT_ALG_HANDLE hAlg = nullptr;
BCRYPT_HASH_HANDLE hHash = nullptr;
if (BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, nullptr, 0) == 0) {
if (BCryptCreateHash(hAlg, &hHash, nullptr, 0, nullptr, 0, 0) == 0) {
if (BCryptHashData(hHash, (PUCHAR)data, (ULONG)len, 0) == 0) {
BCryptFinishHash(hHash, result.data(), 16, 0);
}
BCryptDestroyHash(hHash);
}
BCryptCloseAlgorithmProvider(hAlg, 0);
}
return result;
}
} // namespace MCAuth

70
MCAuth/src/MCAuthCrypto.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
namespace MCAuth {
// Raw ECDSA key pair stored as BCrypt key blobs.
struct ECKeyPair {
int bitSize; // 256 or 384
std::vector<uint8_t> privateBlob; // BCRYPT_ECCPRIVATE_BLOB
std::vector<uint8_t> publicBlob; // BCRYPT_ECCPUBLIC_BLOB
// Raw coordinates extracted from publicBlob (each cbKey bytes, big-endian)
std::vector<uint8_t> x;
std::vector<uint8_t> y;
// SubjectPublicKeyInfo DER encoding of the public key (base64 for MC APIs)
std::vector<uint8_t> publicKeyDER;
};
// Generate fresh key pairs
ECKeyPair GenerateP256KeyPair();
ECKeyPair GenerateP384KeyPair();
// ECDSA-SHA256 over `data`, returns 64-byte P1363 signature (r||s)
std::vector<uint8_t> SignSHA256P256(const ECKeyPair& kp,
const uint8_t* data,
size_t len);
// Base64 (standard alphabet, with padding)
std::string Base64Encode(const uint8_t* data, size_t len);
std::string Base64Encode(const std::vector<uint8_t>& data);
// Base64url (no padding, url-safe alphabet)
std::string Base64UrlEncode(const uint8_t* data, size_t len);
std::string Base64UrlEncode(const std::vector<uint8_t>& data);
// Build the JWK ProofKey JSON fragment for a P-256 key:
// {"kty":"EC","alg":"ES256","crv":"P-256","use":"sig","x":"...","y":"..."}
std::string BuildProofKeyJson(const ECKeyPair& p256);
// Build the XBL "Signature" header value.
// Constructs the signed payload, signs it with the P-256 device key,
// and returns the Base64-encoded header blob.
// method : "POST"
// urlPath : "/device/authenticate" (path + query if any)
// body : raw request body bytes
// authHdr : value of Authorization header (empty string if none)
std::string BuildXblSignatureHeader(const ECKeyPair& p256,
const std::string& method,
const std::string& urlPath,
const std::string& body,
const std::string& authHdr = "");
// Random UUID v4 string (lowercase, dashed)
std::string GenerateUUID();
// Base64 decode (standard alphabet)
std::vector<uint8_t> Base64Decode(const std::string& b64);
std::string Base64DecodeStr(const std::string& b64);
// MD5 hash using Windows BCrypt
std::vector<uint8_t> ComputeMD5(const void* data, size_t len);
// Build SubjectPublicKeyInfo DER for an EC public key (P-256 or P-384)
std::vector<uint8_t> BuildSubjectPublicKeyInfoDER(
int bitSize,
const std::vector<uint8_t>& x,
const std::vector<uint8_t>& y);
} // namespace MCAuth

169
MCAuth/src/MCAuthElyby.cpp Normal file
View file

@ -0,0 +1,169 @@
/*
* MCAuthElyby.cpp Ely.by Yggdrasil authentication
*
* Stateless free functions for username+password accessToken flow.
* Uses the same HTTP/JSON helpers as the rest of MCAuth.
*/
#include "../include/MCAuth.h"
#include "MCAuthCrypto.h"
#include "MCAuthHttp.h"
#include "MCAuthInternal.h"
#include <fstream>
#include <sstream>
namespace MCAuth {
using namespace mcauth;
// ---------------------------------------------------------------------------
// ElybyLogin — POST /auth/authenticate
// ---------------------------------------------------------------------------
bool ElybyLogin(const std::string& username, const std::string& password,
ElybyTokens& outTokens, std::string& error)
{
// Generate clientToken if not already set
std::string clientToken = outTokens.clientToken;
if (clientToken.empty())
clientToken = GenerateUUID();
std::string body = "{\"agent\":{\"name\":\"Minecraft\",\"version\":1}"
",\"username\":" + JsonStr(username) +
",\"password\":" + JsonStr(password) +
",\"clientToken\":" + JsonStr(clientToken) + "}";
try {
auto resp = HttpPost("https://authserver.ely.by/auth/authenticate",
body, "application/json");
if (resp.statusCode == 200 && !resp.body.empty()) {
outTokens.accessToken = JsonGetString(resp.body, "accessToken");
outTokens.clientToken = JsonGetString(resp.body, "clientToken");
// selectedProfile contains id and name
std::string profile = JsonRawValue(resp.body, "selectedProfile");
if (!profile.empty()) {
outTokens.uuid = JsonGetString(profile, "id");
outTokens.username = JsonGetString(profile, "name");
}
if (outTokens.accessToken.empty()) {
error = "ElybyLogin: empty accessToken in response";
return false;
}
return true;
}
// Check for 2FA requirement
if (resp.statusCode == 401) {
std::string errMsg = JsonGetString(resp.body, "errorMessage");
if (errMsg.find("two factor auth") != std::string::npos ||
errMsg.find("Account protected with two factor auth") != std::string::npos) {
error = "elyby_2fa_required";
return false;
}
error = "ElybyLogin: invalid credentials";
if (!errMsg.empty()) error += " (" + errMsg + ")";
return false;
}
error = "ElybyLogin HTTP " + std::to_string(resp.statusCode);
if (!resp.body.empty()) {
std::string msg = JsonGetString(resp.body, "errorMessage");
if (!msg.empty()) error += ": " + msg;
}
} catch (const std::exception& e) {
error = std::string("ElybyLogin network error: ") + e.what();
}
return false;
}
// ---------------------------------------------------------------------------
// ElybyRefresh — POST /auth/refresh
// ---------------------------------------------------------------------------
bool ElybyRefresh(ElybyTokens& tokens, std::string& error)
{
std::string body = "{\"accessToken\":" + JsonStr(tokens.accessToken) +
",\"clientToken\":" + JsonStr(tokens.clientToken) + "}";
try {
auto resp = HttpPost("https://authserver.ely.by/auth/refresh",
body, "application/json");
if (resp.statusCode == 200 && !resp.body.empty()) {
tokens.accessToken = JsonGetString(resp.body, "accessToken");
tokens.clientToken = JsonGetString(resp.body, "clientToken");
std::string profile = JsonRawValue(resp.body, "selectedProfile");
if (!profile.empty()) {
tokens.uuid = JsonGetString(profile, "id");
tokens.username = JsonGetString(profile, "name");
}
return true;
}
error = "ElybyRefresh HTTP " + std::to_string(resp.statusCode);
if (!resp.body.empty()) {
std::string msg = JsonGetString(resp.body, "errorMessage");
if (!msg.empty()) error += ": " + msg;
}
} catch (const std::exception& e) {
error = std::string("ElybyRefresh network error: ") + e.what();
}
return false;
}
// ---------------------------------------------------------------------------
// ElybyValidate — POST /auth/validate
// ---------------------------------------------------------------------------
bool ElybyValidate(const std::string& accessToken, std::string& error)
{
std::string body = "{\"accessToken\":" + JsonStr(accessToken) + "}";
try {
auto resp = HttpPost("https://authserver.ely.by/auth/validate",
body, "application/json");
// 204 = valid, anything else = invalid
return (resp.statusCode == 204 || resp.statusCode == 200);
} catch (const std::exception& e) {
error = std::string("ElybyValidate network error: ") + e.what();
}
return false;
}
// ---------------------------------------------------------------------------
// Token persistence — simple JSON file
// ---------------------------------------------------------------------------
bool ElybyLoadTokens(const std::string& path, ElybyTokens& out)
{
std::ifstream f(path);
if (!f) return false;
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
if (content.empty()) return false;
out.accessToken = JsonGetString(content, "accessToken");
out.clientToken = JsonGetString(content, "clientToken");
out.uuid = JsonGetString(content, "uuid");
out.username = JsonGetString(content, "username");
return !out.accessToken.empty();
}
bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens)
{
std::ostringstream o;
o << "{\n";
o << "\"accessToken\":" << JsonStr(tokens.accessToken) << ",\n";
o << "\"clientToken\":" << JsonStr(tokens.clientToken) << ",\n";
o << "\"uuid\":" << JsonStr(tokens.uuid) << ",\n";
o << "\"username\":" << JsonStr(tokens.username) << "\n";
o << "}";
std::ofstream f(path);
if (!f) return false;
f << o.str();
return f.good();
}
} // namespace MCAuth

221
MCAuth/src/MCAuthHttp.cpp Normal file
View file

@ -0,0 +1,221 @@
#include "MCAuthHttp.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#include <stdexcept>
#include <string>
namespace MCAuth {
// Internal URL parser
struct ParsedUrl {
std::wstring scheme; // L"https"
std::wstring host;
INTERNET_PORT port;
std::wstring path; // includes query string
};
static std::wstring Utf8ToWide(const std::string& s) {
if (s.empty()) return {};
int len = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
std::wstring w(len, L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), w.data(), len);
return w;
}
static std::string WideToUtf8(const std::wstring& w) {
if (w.empty()) return {};
int len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr);
std::string s(len, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), s.data(), len, nullptr, nullptr);
return s;
}
static ParsedUrl ParseUrl(const std::string& url) {
std::wstring wurl = Utf8ToWide(url);
URL_COMPONENTS uc = {};
uc.dwStructSize = sizeof(uc);
wchar_t scheme[16] = {}, host[256] = {}, path[2048] = {};
uc.lpszScheme = scheme; uc.dwSchemeLength = _countof(scheme);
uc.lpszHostName = host; uc.dwHostNameLength = _countof(host);
uc.lpszUrlPath = path; uc.dwUrlPathLength = _countof(path);
if (!WinHttpCrackUrl(wurl.c_str(), (DWORD)wurl.size(), 0, &uc))
throw std::runtime_error("ParseUrl: WinHttpCrackUrl failed for: " + url);
ParsedUrl p;
p.scheme = scheme;
p.host = host;
p.port = uc.nPort;
p.path = path;
if (p.path.empty()) p.path = L"/";
return p;
}
HttpResponse HttpPost(const std::string& url,
const std::string& body,
const std::string& contentType,
const std::map<std::string, std::string>& headers)
{
ParsedUrl parsed = ParseUrl(url);
bool isHttps = (parsed.scheme == L"https");
HINTERNET hSession = WinHttpOpen(
L"MCAuth/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (!hSession)
throw std::runtime_error("HttpPost: WinHttpOpen failed");
// Set explicit timeouts to avoid blocking the caller indefinitely
// (DNS resolve=10s, connect=10s, send=10s, receive=15s)
WinHttpSetTimeouts(hSession, 10000, 10000, 10000, 15000);
HINTERNET hConnect = WinHttpConnect(hSession, parsed.host.c_str(), parsed.port, 0);
if (!hConnect) {
WinHttpCloseHandle(hSession);
throw std::runtime_error("HttpPost: WinHttpConnect failed for host: " + WideToUtf8(parsed.host));
}
DWORD flags = isHttps ? WINHTTP_FLAG_SECURE : 0;
HINTERNET hRequest = WinHttpOpenRequest(
hConnect,
L"POST",
parsed.path.c_str(),
nullptr,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
flags);
if (!hRequest) {
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
throw std::runtime_error("HttpPost: WinHttpOpenRequest failed");
}
// Add Content-Type
std::wstring ct = L"Content-Type: " + Utf8ToWide(contentType);
WinHttpAddRequestHeaders(hRequest, ct.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);
// Add extra headers
for (auto& kv : headers) {
std::wstring h = Utf8ToWide(kv.first) + L": " + Utf8ToWide(kv.second);
WinHttpAddRequestHeaders(hRequest, h.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);
}
BOOL sent = WinHttpSendRequest(
hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS, 0,
const_cast<char*>(body.c_str()), (DWORD)body.size(),
(DWORD)body.size(),
0);
if (!sent || !WinHttpReceiveResponse(hRequest, nullptr)) {
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
throw std::runtime_error("HttpPost: request failed for: " + url);
}
// Read status code
DWORD statusCode = 0;
DWORD statusLen = sizeof(statusCode);
WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&statusCode, &statusLen, WINHTTP_NO_HEADER_INDEX);
// Read body
std::string responseBody;
DWORD bytesAvailable = 0;
while (WinHttpQueryDataAvailable(hRequest, &bytesAvailable) && bytesAvailable > 0) {
std::string chunk(bytesAvailable, '\0');
DWORD bytesRead = 0;
WinHttpReadData(hRequest, chunk.data(), bytesAvailable, &bytesRead);
responseBody.append(chunk.data(), bytesRead);
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return { (int)statusCode, std::move(responseBody) };
}
HttpResponse HttpGet(const std::string& url,
const std::map<std::string, std::string>& headers)
{
ParsedUrl parsed = ParseUrl(url);
bool isHttps = (parsed.scheme == L"https");
HINTERNET hSession = WinHttpOpen(L"MCAuth/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession)
throw std::runtime_error("HttpGet: WinHttpOpen failed");
// Set explicit timeouts to avoid blocking the caller indefinitely
// (DNS resolve=10s, connect=10s, send=10s, receive=15s)
WinHttpSetTimeouts(hSession, 10000, 10000, 10000, 15000);
HINTERNET hConnect = WinHttpConnect(hSession, parsed.host.c_str(), parsed.port, 0);
if (!hConnect) {
WinHttpCloseHandle(hSession);
throw std::runtime_error("HttpGet: WinHttpConnect failed");
}
DWORD flags = isHttps ? WINHTTP_FLAG_SECURE : 0;
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET",
parsed.path.c_str(), nullptr,
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
if (!hRequest) {
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
throw std::runtime_error("HttpGet: WinHttpOpenRequest failed");
}
for (auto& kv : headers) {
std::wstring h = Utf8ToWide(kv.first) + L": " + Utf8ToWide(kv.second);
WinHttpAddRequestHeaders(hRequest, h.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD);
}
if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ||
!WinHttpReceiveResponse(hRequest, nullptr))
{
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
throw std::runtime_error("HttpGet: request failed for: " + url);
}
DWORD statusCode = 0, statusLen = sizeof(statusCode);
WinHttpQueryHeaders(hRequest,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX,
&statusCode, &statusLen, WINHTTP_NO_HEADER_INDEX);
std::string responseBody;
DWORD bytesAvailable = 0;
while (WinHttpQueryDataAvailable(hRequest, &bytesAvailable) && bytesAvailable > 0) {
std::string chunk(bytesAvailable, '\0');
DWORD bytesRead = 0;
WinHttpReadData(hRequest, chunk.data(), bytesAvailable, &bytesRead);
responseBody.append(chunk.data(), bytesRead);
}
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return { (int)statusCode, std::move(responseBody) };
}
} // namespace MCAuth

22
MCAuth/src/MCAuthHttp.h Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <map>
namespace MCAuth {
struct HttpResponse {
int statusCode;
std::string body;
};
// HTTPS POST
HttpResponse HttpPost(const std::string& url,
const std::string& body,
const std::string& contentType,
const std::map<std::string, std::string>& headers = {});
// HTTPS GET
HttpResponse HttpGet(const std::string& url,
const std::map<std::string, std::string>& headers = {});
} // namespace MCAuth

205
MCAuth/src/MCAuthInternal.h Normal file
View file

@ -0,0 +1,205 @@
#pragma once
#include <string>
#include <vector>
#include <map>
#include <cstdint>
#include <chrono>
#include <ctime>
#include <cstdio>
#include <cctype>
namespace mcauth {
inline std::string JsonRawValue(const std::string& json, const std::string& key) {
std::string search = "\"" + key + "\"";
size_t pos = 0;
while ((pos = json.find(search, pos)) != std::string::npos) {
size_t colon = json.find_first_not_of(" \t\r\n", pos + search.size());
if (colon == std::string::npos || json[colon] != ':') { pos++; continue; }
size_t vstart = json.find_first_not_of(" \t\r\n", colon + 1);
if (vstart == std::string::npos) return "";
char c = json[vstart];
if (c == '"') {
size_t end = vstart + 1;
while (end < json.size()) {
if (json[end] == '\\') { end += 2; continue; }
if (json[end] == '"') { end++; break; }
end++;
}
return json.substr(vstart, end - vstart);
} else if (c == '{' || c == '[') {
char open = c, close = (c == '{') ? '}' : ']';
int depth = 1;
size_t end = vstart + 1;
while (end < json.size() && depth > 0) {
if (json[end] == open) depth++;
else if (json[end] == close) depth--;
else if (json[end] == '"') {
end++;
while (end < json.size() && json[end] != '"') {
if (json[end] == '\\') end++;
end++;
}
}
end++;
}
return json.substr(vstart, end - vstart);
} else {
size_t end = json.find_first_of(",}\r\n", vstart);
if (end == std::string::npos) end = json.size();
std::string v = json.substr(vstart, end - vstart);
while (!v.empty() && isspace((unsigned char)v.back())) v.pop_back();
return v;
}
}
return "";
}
inline std::string JsonUnquote(const std::string& s) {
if (s.size() >= 2 && s.front() == '"' && s.back() == '"')
return s.substr(1, s.size() - 2);
return s;
}
inline std::string JsonGetString(const std::string& json, const std::string& key) {
return JsonUnquote(JsonRawValue(json, key));
}
inline int64_t JsonGetInt(const std::string& json, const std::string& key) {
auto s = JsonUnquote(JsonRawValue(json, key));
if (s.empty()) return 0;
try { return std::stoll(s); } catch (...) { return 0; }
}
inline std::string JsonFirstArrayObject(const std::string& arrStr) {
size_t pos = arrStr.find('[');
if (pos == std::string::npos) return "";
pos = arrStr.find_first_not_of(" \t\r\n", pos + 1);
if (pos == std::string::npos || arrStr[pos] == ']') return "";
if (arrStr[pos] != '{') return "";
int depth = 1;
size_t end = pos + 1;
while (end < arrStr.size() && depth > 0) {
if (arrStr[end] == '{') depth++;
else if (arrStr[end] == '}') depth--;
else if (arrStr[end] == '"') {
end++;
while (end < arrStr.size() && arrStr[end] != '"') {
if (arrStr[end] == '\\') end++;
end++;
}
}
end++;
}
return arrStr.substr(pos, end - pos);
}
inline std::string JsonNthArrayString(const std::string& arrStr, int n) {
size_t pos = arrStr.find('[');
if (pos == std::string::npos) return "";
pos++;
int idx = 0;
while (pos < arrStr.size()) {
pos = arrStr.find_first_not_of(" \t\r\n,", pos);
if (pos == std::string::npos || arrStr[pos] == ']') break;
if (arrStr[pos] == '"') {
size_t end = pos + 1;
while (end < arrStr.size()) {
if (arrStr[end] == '\\') { end += 2; continue; }
if (arrStr[end] == '"') { end++; break; }
end++;
}
if (idx == n) return arrStr.substr(pos + 1, end - pos - 2);
pos = end;
idx++;
} else {
pos++;
}
}
return "";
}
inline std::vector<std::string> JsonArrayObjects(const std::string& json) {
std::vector<std::string> result;
size_t pos = json.find('[');
if (pos == std::string::npos) return result;
++pos;
while (pos < json.size()) {
pos = json.find('{', pos);
if (pos == std::string::npos) break;
int depth = 0;
size_t start = pos;
for (; pos < json.size(); ++pos) {
if (json[pos] == '{') ++depth;
else if (json[pos] == '}') { --depth; if (depth == 0) { ++pos; break; } }
}
result.push_back(json.substr(start, pos - start));
}
return result;
}
inline std::string UrlEncode(const std::string& s) {
std::string out;
out.reserve(s.size() * 3);
for (unsigned char c : s) {
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
out += c;
} else {
char buf[4];
snprintf(buf, sizeof(buf), "%%%02X", c);
out += buf;
}
}
return out;
}
inline int64_t NowMs() {
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
inline std::string BuildFormBody(const std::map<std::string, std::string>& params) {
std::string body;
for (auto& kv : params) {
if (!body.empty()) body += '&';
body += UrlEncode(kv.first) + '=' + UrlEncode(kv.second);
}
return body;
}
inline std::string JsonEscape(const std::string& s) {
std::string out;
for (char c : s) {
if (c == '"') out += "\\\"";
else if (c == '\\') out += "\\\\";
else if (c == '\n') out += "\\n";
else if (c == '\r') out += "\\r";
else out += c;
}
return out;
}
inline std::string JsonStr(const std::string& s) {
return "\"" + JsonEscape(s) + "\"";
}
inline int64_t ParseIso8601Ms(const std::string& s) {
int Y=0, M=0, D=0, h=0, m=0;
double sec=0.0;
if (sscanf_s(s.c_str(), "%d-%d-%dT%d:%d:%lf", &Y, &M, &D, &h, &m, &sec) < 6)
return 0;
struct tm t = {};
t.tm_year = Y - 1900;
t.tm_mon = M - 1;
t.tm_mday = D;
t.tm_hour = h;
t.tm_min = m;
t.tm_sec = (int)sec;
t.tm_isdst = 0;
time_t epoch = _mkgmtime(&t);
if (epoch == (time_t)-1) return 0;
int ms = (int)((sec - (int)sec) * 1000.0);
return (int64_t)epoch * 1000LL + ms;
}
} // namespace mcauth

453
MCAuth/src/MCAuthJava.cpp Normal file
View file

@ -0,0 +1,453 @@
/*
* MCAuthJava.cpp Minecraft Java Edition authentication chain
*
* Chain (using Java Win32 Title ID "00000000402b5328" via SISU):
*
* 1. MSA Device Code POST login.live.com/oauth20_connect.srf
* 2. MSA Token poll login.live.com/oauth20_token.srf
* 3. XBL Device Auth POST device.auth.xboxlive.com (signed P-256)
* 4. SISU Authorize POST sisu.xboxlive.com (signed P-256)
* RelyingParty = "rp://api.minecraftservices.com/"
* JavaXstsToken {token, uhs}
* 5. Launcher Login POST api.minecraftservices.com/launcher/login
* xtoken = "XBL3.0 x=<uhs>;<token>"
* access_token, token_type, expires_in
* 6. Profile GET api.minecraftservices.com/minecraft/profile
* Authorization: <token_type> <access_token>
* id (undashed UUID), name
*/
#include "../include/MCAuth.h"
#include "MCAuthCrypto.h"
#include "MCAuthHttp.h"
#include "MCAuthInternal.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <ctime>
#include <map>
namespace MCAuth {
using namespace mcauth;
struct JavaAuthManager::Impl {
// Persistent
struct MsaState {
std::string accessToken, refreshToken;
int64_t expireMs = 0;
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} msa;
ECKeyPair deviceKeyP256;
std::string deviceId;
// Derived / cached
struct XblDeviceState {
std::string token, did;
int64_t expireMs = 0;
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} xblDevice;
struct JavaXstsState {
std::string token, uhs;
int64_t expireMs = 0;
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} javaXsts;
struct MinecraftTokenState {
std::string tokenType, accessToken;
int64_t expireMs = 0;
std::string AuthHeader() const { return tokenType + " " + accessToken; }
bool IsExpired() const { return NowMs() >= expireMs - 60000; }
} mcToken;
std::atomic<bool> loggedIn{false};
// Cancellation: condition_variable for instant wakeup from poll sleep
std::atomic<bool> cancelRequested{false};
std::mutex cancelMutex;
std::condition_variable cancelCv;
// Waitable sleep that returns immediately on cancel.
// Returns true if cancel was requested.
bool SleepOrCancel(int64_t ms) {
std::unique_lock<std::mutex> lock(cancelMutex);
return cancelCv.wait_for(lock, std::chrono::milliseconds(ms), [this] {
return cancelRequested.load(std::memory_order_relaxed);
});
}
Impl() {
deviceKeyP256 = GenerateP256KeyPair();
deviceId = GenerateUUID();
}
// Step 1: Request MSA device code
struct DevCodeState {
std::string deviceCode, userCode, verificationUri;
int64_t expiresMs, intervalMs;
};
DevCodeState RequestDeviceCode() {
auto resp = HttpPost(
"https://login.live.com/oauth20_connect.srf",
BuildFormBody({{"client_id", JAVA_CLIENT_ID},
{"scope", MSA_SCOPE}, // same scope as Bedrock
{"response_type", "device_code"}}),
"application/x-www-form-urlencoded");
if (resp.statusCode != 200)
throw std::runtime_error("Java DeviceCode HTTP " +
std::to_string(resp.statusCode) + ": " + resp.body);
DevCodeState dc;
dc.deviceCode = JsonGetString(resp.body, "device_code");
dc.userCode = JsonGetString(resp.body, "user_code");
dc.verificationUri = JsonGetString(resp.body, "verification_uri");
if (dc.deviceCode.empty() || dc.userCode.empty() || dc.verificationUri.empty())
throw std::runtime_error("Java DeviceCode: missing required fields in response");
int64_t expiresIn = JsonGetInt(resp.body, "expires_in");
int64_t intervalIn = JsonGetInt(resp.body, "interval");
dc.expiresMs = NowMs() + (expiresIn > 0 ? expiresIn : 300) * 1000;
dc.intervalMs = (intervalIn > 0 ? intervalIn : 5) * 1000;
return dc;
}
// Step 2: Poll for MSA token
bool PollMsaToken(const DevCodeState& dc, int timeoutSec, std::string& error) {
int64_t deadline = NowMs() + (int64_t)timeoutSec * 1000;
int64_t intervalMs = dc.intervalMs > 0 ? dc.intervalMs : 5000;
while (NowMs() < deadline && NowMs() < dc.expiresMs) {
if (SleepOrCancel(intervalMs)) {
error = "cancelled";
return false;
}
HttpResponse resp;
try {
resp = HttpPost(
"https://login.live.com/oauth20_token.srf",
BuildFormBody({{"client_id", JAVA_CLIENT_ID},
{"scope", MSA_SCOPE},
{"grant_type", "device_code"},
{"device_code", dc.deviceCode}}),
"application/x-www-form-urlencoded");
} catch (const std::exception& ex) {
error = std::string("MSA poll network error: ") + ex.what();
return false;
}
if (resp.statusCode == 200) {
msa.accessToken = JsonGetString(resp.body, "access_token");
msa.refreshToken = JsonGetString(resp.body, "refresh_token");
msa.expireMs = NowMs() + JsonGetInt(resp.body, "expires_in") * 1000;
return true;
}
std::string err = JsonGetString(resp.body, "error");
if (err == "authorization_pending") continue;
if (err == "slow_down") { intervalMs += 5000; continue; }
error = "MSA poll error: " + err;
return false;
}
error = "MSA device code login timed out";
return false;
}
// Step 2b: Refresh MSA token
bool RefreshMsaToken(std::string& error) {
if (msa.refreshToken.empty()) { error = "No refresh token"; return false; }
HttpResponse resp;
try {
resp = HttpPost(
"https://login.live.com/oauth20_token.srf",
BuildFormBody({{"client_id", JAVA_CLIENT_ID},
{"scope", MSA_SCOPE},
{"grant_type", "refresh_token"},
{"refresh_token", msa.refreshToken}}),
"application/x-www-form-urlencoded");
} catch (const std::exception& ex) {
error = std::string("MSA refresh network error: ") + ex.what();
return false;
}
if (resp.statusCode != 200) {
error = "MSA refresh HTTP " + std::to_string(resp.statusCode);
return false;
}
msa.accessToken = JsonGetString(resp.body, "access_token");
std::string nr = JsonGetString(resp.body, "refresh_token");
if (!nr.empty()) msa.refreshToken = nr;
msa.expireMs = NowMs() + JsonGetInt(resp.body, "expires_in") * 1000;
return true;
}
// Step 3: XBL Device Authentication
bool AuthXblDevice(std::string& error) {
std::string proofKey = BuildProofKeyJson(deviceKeyP256);
std::string bodyJson =
"{\"Properties\":{"
"\"DeviceType\":\"Win32\","
"\"Id\":\"{" + deviceId + "}\","
"\"AuthMethod\":\"ProofOfPossession\","
"\"ProofKey\":" + proofKey +
"},\"RelyingParty\":\"http://auth.xboxlive.com\","
"\"TokenType\":\"JWT\"}";
std::string sig = BuildXblSignatureHeader(
deviceKeyP256, "POST", "/device/authenticate", bodyJson);
auto resp = HttpPost(
"https://device.auth.xboxlive.com/device/authenticate",
bodyJson, "application/json",
{{"x-xbl-contract-version","1"},{"Signature", sig}});
if (resp.statusCode != 200) {
error = "XBL device auth HTTP " + std::to_string(resp.statusCode);
return false;
}
xblDevice.token = JsonGetString(resp.body, "Token");
xblDevice.expireMs = ParseIso8601Ms(JsonGetString(resp.body, "NotAfter"));
std::string xdi = JsonRawValue(JsonRawValue(resp.body, "DisplayClaims"), "xdi");
xblDevice.did = JsonGetString(xdi, "did");
return true;
}
// Step 4: SISU Authorize — Java relying party
bool AuthSisu(std::string& error) {
std::string proofKey = BuildProofKeyJson(deviceKeyP256);
std::string bodyJson =
"{\"Sandbox\":\"RETAIL\","
"\"UseModernGamertag\":true,"
"\"AppId\":\"" + std::string(JAVA_CLIENT_ID) + "\","
"\"AccessToken\":\"t=" + msa.accessToken + "\","
"\"DeviceToken\":\"" + xblDevice.token + "\","
"\"ProofKey\":" + proofKey + ","
// Java Edition relying party
"\"RelyingParty\":\"rp://api.minecraftservices.com/\"}";
std::string sig = BuildXblSignatureHeader(
deviceKeyP256, "POST", "/authorize", bodyJson);
auto resp = HttpPost(
"https://sisu.xboxlive.com/authorize",
bodyJson, "application/json", {{"Signature", sig}});
if (resp.statusCode != 200) {
error = "SISU authorize HTTP " + std::to_string(resp.statusCode)
+ ": " + resp.body;
return false;
}
// AuthorizationToken → JavaXstsToken
std::string authObj = JsonRawValue(resp.body, "AuthorizationToken");
javaXsts.token = JsonGetString(authObj, "Token");
javaXsts.expireMs = ParseIso8601Ms(JsonGetString(authObj, "NotAfter"));
std::string disp = JsonRawValue(authObj, "DisplayClaims");
std::string xui = JsonRawValue(disp, "xui");
std::string xui0 = JsonFirstArrayObject(xui);
javaXsts.uhs = JsonGetString(xui0, "uhs");
if (javaXsts.token.empty() || javaXsts.uhs.empty()) {
error = "SISU response missing XSTS token or uhs";
return false;
}
return true;
}
// Step 5: Minecraft Launcher Login
bool AuthMcLauncherLogin(std::string& error) {
std::string xtoken = "XBL3.0 x=" + javaXsts.uhs + ";" + javaXsts.token;
std::string bodyJson =
"{\"platform\":\"PC_LAUNCHER\","
"\"xtoken\":\"" + xtoken + "\"}";
auto resp = HttpPost(
"https://api.minecraftservices.com/launcher/login",
bodyJson, "application/json");
if (resp.statusCode != 200) {
error = "Launcher login HTTP " + std::to_string(resp.statusCode)
+ ": " + resp.body;
return false;
}
mcToken.tokenType = JsonGetString(resp.body, "token_type");
mcToken.accessToken = JsonGetString(resp.body, "access_token");
mcToken.expireMs = NowMs() + JsonGetInt(resp.body, "expires_in") * 1000;
return true;
}
// Step 6: Minecraft Profile
bool FetchProfile(JavaSession& out, std::string& error) {
auto resp = HttpGet(
"https://api.minecraftservices.com/minecraft/profile",
{{"Authorization", mcToken.AuthHeader()}});
if (resp.statusCode == 404) {
// Account exists but doesn't own Minecraft Java Edition
error = "This Microsoft account does not own Minecraft Java Edition";
return false;
}
if (resp.statusCode != 200) {
error = "Profile HTTP " + std::to_string(resp.statusCode)
+ ": " + resp.body;
return false;
}
std::string rawId = JsonGetString(resp.body, "id");
out.uuid = DashUuid(rawId);
out.username = JsonGetString(resp.body, "name");
out.accessToken = mcToken.AuthHeader();
out.expireMs = mcToken.expireMs;
if (out.uuid.empty() || out.username.empty()) {
error = "Minecraft profile response missing uuid or username";
return false;
}
return true;
}
// Full login chain
bool DoFullAuth(DeviceCodeCallback onDeviceCode,
JavaSession& out, std::string& error, int timeoutSec) {
cancelRequested.store(false, std::memory_order_relaxed);
DevCodeState dc;
try { dc = RequestDeviceCode(); }
catch (const std::exception& ex) {
error = std::string("DeviceCode network error: ") + ex.what();
return false;
}
DeviceCodeInfo info;
info.userCode = dc.userCode;
info.verificationUri = dc.verificationUri;
info.directUri = dc.verificationUri + "?otc=" + dc.userCode;
info.deviceCode = dc.deviceCode;
info.expiresMs = dc.expiresMs;
info.intervalMs = dc.intervalMs;
onDeviceCode(info);
if (!PollMsaToken(dc, timeoutSec, error)) return false;
return DoAuthChain(out, error);
}
bool DoAuthChain(JavaSession& out, std::string& error) {
try {
if (!AuthXblDevice(error)) return false;
if (!AuthSisu(error)) return false;
if (!AuthMcLauncherLogin(error)) return false;
if (!FetchProfile(out, error)) return false;
} catch (const std::runtime_error& ex) {
// Network/crypto exceptions carry context; propagate as-is
error = ex.what();
return false;
} catch (const std::exception& ex) {
error = std::string("Unexpected auth error: ") + ex.what();
return false;
} catch (...) {
error = "Unknown internal error during auth chain";
return false;
}
loggedIn = true;
return true;
}
// Serialisation helpers
std::string Serialise() const {
std::ostringstream o;
o << "{\n"
<< "\"msaAccessToken\":" << mcauth::JsonStr(msa.accessToken) << ",\n"
<< "\"msaRefreshToken\":" << mcauth::JsonStr(msa.refreshToken) << ",\n"
<< "\"msaExpireMs\":" << msa.expireMs << ",\n"
<< "\"deviceId\":" << mcauth::JsonStr(deviceId) << ",\n"
<< "\"deviceKeyPriv\":" << mcauth::JsonStr(Base64Encode(deviceKeyP256.privateBlob)) << ",\n"
<< "\"deviceKeyPub\":" << mcauth::JsonStr(Base64Encode(deviceKeyP256.publicBlob)) << "\n"
<< "}";
return o.str();
}
static ECKeyPair RebuildP256(const std::vector<uint8_t>& priv,
const std::vector<uint8_t>& pub) {
if (pub.size() < sizeof(BCRYPT_ECCKEY_BLOB)) return {};
const BCRYPT_ECCKEY_BLOB* hdr =
reinterpret_cast<const BCRYPT_ECCKEY_BLOB*>(pub.data());
ULONG cbKey = hdr->cbKey;
if (pub.size() < 8 + (size_t)cbKey * 2) return {};
std::vector<uint8_t> x(pub.begin()+8, pub.begin()+8+cbKey);
std::vector<uint8_t> y(pub.begin()+8+cbKey, pub.begin()+8+cbKey*2);
ECKeyPair kp;
kp.bitSize = 256; kp.privateBlob = priv; kp.publicBlob = pub;
kp.x = x; kp.y = y;
kp.publicKeyDER = BuildSubjectPublicKeyInfoDER(256, x, y);
return kp;
}
bool Deserialise(const std::string& json) {
msa.accessToken = JsonGetString(json, "msaAccessToken");
msa.refreshToken = JsonGetString(json, "msaRefreshToken");
msa.expireMs = JsonGetInt(json, "msaExpireMs");
deviceId = JsonGetString(json, "deviceId");
auto priv = Base64Decode(JsonGetString(json, "deviceKeyPriv"));
auto pub = Base64Decode(JsonGetString(json, "deviceKeyPub"));
if (priv.empty() || pub.empty()) return false;
deviceKeyP256 = RebuildP256(priv, pub);
return true;
}
};
JavaAuthManager::JavaAuthManager() : m_impl(std::make_unique<Impl>()) {}
JavaAuthManager::~JavaAuthManager() = default;
bool JavaAuthManager::Login(DeviceCodeCallback onDeviceCode,
JavaSession& out, std::string& error, int timeoutSec)
{
return m_impl->DoFullAuth(onDeviceCode, out, error, timeoutSec);
}
bool JavaAuthManager::Refresh(JavaSession& out, std::string& error) {
m_impl->cancelRequested.store(false, std::memory_order_relaxed);
if (!m_impl->RefreshMsaToken(error)) return false;
return m_impl->DoAuthChain(out, error);
}
bool JavaAuthManager::IsLoggedIn() const { return m_impl->loggedIn; }
void JavaAuthManager::Logout() {
m_impl->msa = {};
m_impl->javaXsts = {};
m_impl->mcToken = {};
m_impl->loggedIn = false;
}
void JavaAuthManager::RequestCancel() {
m_impl->cancelRequested.store(true, std::memory_order_release);
m_impl->cancelCv.notify_all();
}
bool JavaAuthManager::SaveTokens(const std::string& path) const {
std::ofstream f(path);
if (!f) return false;
f << m_impl->Serialise();
return f.good();
}
bool JavaAuthManager::LoadTokens(const std::string& path) {
std::ifstream f(path);
if (!f) return false;
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
return m_impl->Deserialise(content);
}
} // namespace MCAuth

View file

@ -0,0 +1,986 @@
#define _CRT_SECURE_NO_WARNINGS
#include "../include/MCAuthManager.h"
#include "MCAuthInternal.h"
#include <cstdio>
#include <sstream>
#include <fstream>
#include <chrono>
#include <thread>
#ifdef _WIN32
#include <Windows.h>
#endif
static void AuthLogImpl(const char* fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
#ifdef _WIN32
OutputDebugStringA(buf);
#endif
// Use _fsopen with _SH_DENYWR so only this process can write
FILE* f = _fsopen("mcauth_debug.log", "a", _SH_DENYWR);
if (f) { fputs(buf, f); fclose(f); }
}
#define AUTH_LOG(msg, ...) AuthLogImpl("[MCAuth] " msg "\n", ##__VA_ARGS__)
using namespace mcauth;
MCAuthManager& MCAuthManager::Get() {
static MCAuthManager instance;
return instance;
}
MCAuthManager::MCAuthManager() = default;
MCAuthManager::~MCAuthManager() {
// Cancel all in-flight workers so detached threads exit promptly.
for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
++m_slots[i].generation;
if (m_slots[i].auth)
m_slots[i].auth->RequestCancel();
}
}
std::shared_ptr<MCAuth::JavaAuthManager> MCAuthManager::ResetSlotAuth(AuthSlot& s) {
// Cancel the old engine's blocking operations
if (s.auth)
s.auth->RequestCancel();
// Create a fresh engine — old threads still hold their shared_ptr copy
auto fresh = std::make_shared<MCAuth::JavaAuthManager>();
s.auth = fresh;
return fresh;
}
MCAuth::JavaSession MCAuthManager::SynthesizeOfflineSession(const JavaAccountInfo& acct) {
MCAuth::JavaSession s;
s.username = acct.username;
s.uuid = acct.uuid;
s.accessToken = "";
s.expireMs = 0;
return s;
}
bool MCAuthManager::LoadJavaAccountIndex() {
std::ifstream f(kJavaAccountsFile);
if (!f) {
AUTH_LOG("LoadJavaAccountIndex: file '%s' not found", kJavaAccountsFile);
return false;
}
std::string content((std::istreambuf_iterator<char>(f)),
std::istreambuf_iterator<char>());
std::lock_guard<std::mutex> lock(m_accountsMutex);
m_javaAccounts.clear();
int activeIndex = (int)JsonGetInt(content, "activeIndex");
// Parse accounts array
std::vector<std::string> objects;
{
size_t pos = content.find("\"accounts\"");
if (pos != std::string::npos) {
pos = content.find('[', pos);
if (pos != std::string::npos) {
int depth = 0; size_t start = pos;
for (; pos < content.size(); ++pos) {
if (content[pos] == '[') ++depth;
else if (content[pos] == ']') { --depth; if (depth == 0) { ++pos; break; } }
}
std::string arr = content.substr(start, pos - start);
objects = JsonArrayObjects(arr);
}
}
}
for (auto& obj : objects) {
JavaAccountInfo info;
info.username = JsonGetString(obj, "username");
info.uuid = JsonGetString(obj, "uuid");
info.tokenFile = JsonGetString(obj, "tokenFile");
info.isOffline = (JsonRawValue(obj, "isOffline") == "true");
info.authProvider = JsonGetString(obj, "authProvider");
if (info.authProvider.empty())
info.authProvider = info.isOffline ? "offline" : "mojang";
if (!info.tokenFile.empty() || info.isOffline)
m_javaAccounts.push_back(std::move(info));
}
// Set slot 0's active account from saved index
if (activeIndex >= 0 && activeIndex < (int)m_javaAccounts.size())
m_slots[0].accountIndex = activeIndex;
else
m_slots[0].accountIndex = m_javaAccounts.empty() ? -1 : 0;
AUTH_LOG("LoadJavaAccountIndex: loaded %d accounts, active=%d",
(int)m_javaAccounts.size(), m_slots[0].accountIndex.load());
return true;
}
bool MCAuthManager::SaveJavaAccountIndex() const {
std::lock_guard<std::mutex> lock(m_accountsMutex);
std::ostringstream o;
o << "{\n";
o << "\"activeIndex\":" << m_slots[0].accountIndex.load() << ",\n";
o << "\"accounts\":[\n";
for (size_t i = 0; i < m_javaAccounts.size(); ++i) {
auto& a = m_javaAccounts[i];
o << " {\"username\":" << JsonStr(a.username)
<< ",\"uuid\":" << JsonStr(a.uuid)
<< ",\"tokenFile\":" << JsonStr(a.tokenFile)
<< ",\"isOffline\":" << (a.isOffline ? "true" : "false")
<< ",\"authProvider\":" << JsonStr(a.authProvider.empty() ? "mojang" : a.authProvider) << "}";
if (i + 1 < m_javaAccounts.size()) o << ",";
o << "\n";
}
o << "]\n}";
std::ofstream f(kJavaAccountsFile);
if (!f) return false;
f << o.str();
AUTH_LOG("SaveJavaAccountIndex: saved %d accounts, active=%d",
(int)m_javaAccounts.size(), m_slots[0].accountIndex.load());
return f.good();
}
std::vector<MCAuthManager::JavaAccountInfo> MCAuthManager::GetJavaAccounts() const {
std::lock_guard<std::mutex> lock(m_accountsMutex);
return m_javaAccounts;
}
std::string MCAuthManager::AllocTokenFile(const std::string& uuid) const {
if (!uuid.empty())
return uuid + ".json";
for (int i = 0; ; ++i) {
char buf[64];
snprintf(buf, sizeof(buf), "java_auth_%d.json", i);
std::string name(buf);
bool taken = false;
for (auto& a : m_javaAccounts) {
if (a.tokenFile == name) { taken = true; break; }
}
if (!taken) return name;
}
}
const MCAuthManager::AuthSlot& MCAuthManager::GetSlot(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
return m_slots[slot];
}
MCAuth::JavaSession MCAuthManager::GetSlotSession(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0;
auto& s = m_slots[slot];
// Check offline status first (accounts mutex), then get session (slot mutex).
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()
&& m_javaAccounts[s.accountIndex].isOffline)
return SynthesizeOfflineSession(m_javaAccounts[s.accountIndex]);
}
std::lock_guard<std::mutex> lock(s.mutex);
return s.session;
}
bool MCAuthManager::IsSlotLoggedIn(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
if (m_javaAccounts[s.accountIndex].isOffline)
return true;
if (m_javaAccounts[s.accountIndex].authProvider == "elyby") {
std::lock_guard<std::mutex> slock(s.mutex);
return !s.elybyTokens.accessToken.empty();
}
}
}
return s.auth && s.auth->IsLoggedIn();
}
bool MCAuthManager::IsTokenExpiringSoon(int slot) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
std::lock_guard<std::mutex> lock(s.mutex);
if (s.session.expireMs <= 0) return false;
return NowMs() > s.session.expireMs - 60000; // within 60s of expiry
}
bool MCAuthManager::IsAccountInUseByOtherSlot(int slot, int accountIndex) const {
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (accountIndex < 0 || accountIndex >= (int)m_javaAccounts.size()) return false;
for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
if (i == slot) continue;
if (m_slots[i].accountIndex == accountIndex) return true;
}
return false;
}
void MCAuthManager::RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile,
bool failOpen, bool alwaysSaveIndex) {
auto& s = m_slots[slot];
std::string error;
MCAuth::ElybyTokens tokens;
{
std::lock_guard<std::mutex> lock(s.mutex);
tokens = s.elybyTokens;
}
bool ok = MCAuth::ElybyRefresh(tokens, error);
if (s.generation != gen) return;
{
std::lock_guard<std::mutex> lock(s.mutex);
if (ok) {
s.elybyTokens = tokens;
s.session.username = tokens.username;
s.session.uuid = MCAuth::DashUuid(tokens.uuid);
s.session.accessToken = tokens.accessToken;
s.lastError.clear();
s.state = State::Success;
} else {
s.lastError = error;
s.state = failOpen ? State::Success : State::Failed;
}
s.cv.notify_all();
}
if (ok) {
if (!tokenFile.empty())
MCAuth::ElybySaveTokens(tokenFile, tokens);
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
m_javaAccounts[s.accountIndex].username = tokens.username;
m_javaAccounts[s.accountIndex].uuid = MCAuth::DashUuid(tokens.uuid);
}
}
}
if (ok || alwaysSaveIndex)
SaveJavaAccountIndex();
}
void MCAuthManager::RefreshSlot(int slot) {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return;
auto& s = m_slots[slot];
// Check if this is an ely.by account — if so, use ely.by refresh
std::string provider;
std::string elyTokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
provider = m_javaAccounts[s.accountIndex].authProvider;
elyTokenFile = m_javaAccounts[s.accountIndex].tokenFile;
}
}
if (provider == "elyby") {
uint32_t gen = ++s.generation;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
std::thread([this, slot, gen, elyTokenFile]() {
RunElybyRefresh(slot, gen, elyTokenFile, /*failOpen=*/false, /*alwaysSaveIndex=*/false);
}).detach();
return;
}
// Increment generation to invalidate any in-flight work
uint32_t gen = ++s.generation;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
// Capture auth engine by shared_ptr so it stays alive if replaced.
auto authCopy = s.auth;
std::thread([this, slot, gen, authCopy]() {
auto& s = m_slots[slot];
std::string error;
MCAuth::JavaSession session;
bool ok = false;
try {
ok = authCopy->Refresh(session, error);
} catch (const std::exception& ex) {
error = std::string("RefreshSlot network error: ") + ex.what();
} catch (...) {
error = "RefreshSlot unknown error";
}
// Stale check: if a newer operation started, discard silently
if (s.generation != gen) {
AUTH_LOG("RefreshSlot(%d): generation mismatch (%u vs %u), discarding",
slot, gen, s.generation.load());
return;
}
std::string tokenFile;
{
std::lock_guard<std::mutex> lock(s.mutex);
if (ok) {
s.session = session;
s.lastError.clear();
s.state = State::Success;
} else {
s.lastError = error;
s.state = State::Failed;
}
s.cv.notify_all();
}
// Update account info outside slot mutex (consistent lock order)
if (ok) {
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) {
m_javaAccounts[s.accountIndex].username = session.username;
m_javaAccounts[s.accountIndex].uuid = session.uuid;
tokenFile = m_javaAccounts[s.accountIndex].tokenFile;
}
}
AUTH_LOG("RefreshSlot(%d): ok=%d, username='%s', error='%s'",
slot, (int)ok, ok ? session.username.c_str() : "", error.c_str());
// File I/O outside all mutexes
if (ok && !tokenFile.empty()) {
authCopy->SaveTokens(tokenFile);
SaveJavaAccountIndex();
}
}).detach();
}
bool MCAuthManager::SetAccountForSlot(int slot, int accountIndex) {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
bool isOffline = false;
bool isElyby = false;
std::string tokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (accountIndex < 0 || accountIndex >= (int)m_javaAccounts.size()) return false;
isOffline = m_javaAccounts[accountIndex].isOffline;
isElyby = (m_javaAccounts[accountIndex].authProvider == "elyby");
tokenFile = m_javaAccounts[accountIndex].tokenFile;
}
// Invalidate any in-flight work for this slot
uint32_t gen = ++s.generation;
if (isOffline) {
// Offline: set session immediately, no background work.
MCAuth::JavaSession offlineSession;
std::string offlineName;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
offlineSession = SynthesizeOfflineSession(m_javaAccounts[accountIndex]);
offlineName = m_javaAccounts[accountIndex].username;
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.accountIndex = accountIndex;
s.session = offlineSession;
s.state = State::Success;
s.cv.notify_all();
}
AUTH_LOG("SetAccountForSlot(%d): switched to offline account %d '%s'",
slot, accountIndex, offlineName.c_str());
(void)gen;
} else if (isElyby) {
// Ely.by: load tokens from file, build session, refresh in background
{
std::lock_guard<std::mutex> lock(s.mutex);
s.accountIndex = accountIndex;
}
AUTH_LOG("SetAccountForSlot(%d): switching to elyby account %d, tokenFile='%s'",
slot, accountIndex, tokenFile.c_str());
MCAuth::ElybyTokens elyTokens;
if (!MCAuth::ElybyLoadTokens(tokenFile, elyTokens)) {
AUTH_LOG("SetAccountForSlot(%d): ElybyLoadTokens failed", slot);
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.cv.notify_all();
return false;
}
// Store tokens and build session
{
std::lock_guard<std::mutex> lock(s.mutex);
s.elybyTokens = elyTokens;
s.session.username = elyTokens.username;
s.session.uuid = MCAuth::DashUuid(elyTokens.uuid);
s.session.accessToken = elyTokens.accessToken;
s.session.expireMs = 0; // Ely.by tokens don't have a fixed expiry
s.state = State::Authenticating;
s.cv.notify_all();
}
// Refresh in background
uint32_t elyGen = ++s.generation;
std::thread([this, slot, elyGen, tokenFile]() {
RunElybyRefresh(slot, elyGen, tokenFile, /*failOpen=*/true, /*alwaysSaveIndex=*/true);
}).detach();
} else {
// Online (Mojang/Microsoft): create a fresh auth engine, load tokens, refresh in background
{
std::lock_guard<std::mutex> lock(s.mutex);
s.accountIndex = accountIndex;
}
AUTH_LOG("SetAccountForSlot(%d): switching to online account %d, tokenFile='%s'",
slot, accountIndex, tokenFile.c_str());
auto freshAuth = ResetSlotAuth(s);
if (!freshAuth->LoadTokens(tokenFile)) {
AUTH_LOG("SetAccountForSlot(%d): LoadTokens failed", slot);
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.cv.notify_all();
return false;
}
// Refresh in background (uses the fresh auth engine via s.auth)
RefreshSlot(slot);
}
SaveJavaAccountIndex();
return true;
}
void MCAuthManager::ClearSlot(int slot) {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return;
auto& s = m_slots[slot];
++s.generation; // invalidate in-flight work
ResetSlotAuth(s);
std::lock_guard<std::mutex> lock(s.mutex);
s.session = {};
s.accountIndex = -1;
s.state = State::Idle;
s.lastError.clear();
s.cv.notify_all();
}
bool MCAuthManager::WaitForSlotReady(int slot, int timeoutMs) const {
if (slot < 0 || slot >= XUSER_MAX_COUNT) return false;
auto& s = m_slots[slot];
std::unique_lock<std::mutex> lock(s.mutex);
bool ok = s.cv.wait_for(lock, std::chrono::milliseconds(timeoutMs), [&] {
State st = s.state.load();
return st != State::Authenticating && st != State::WaitingForCode;
});
return ok && (s.state == State::Success);
}
int MCAuthManager::GetActiveJavaAccountIndex() const {
return m_slots[0].accountIndex;
}
MCAuth::JavaSession MCAuthManager::GetJavaSession() const {
return GetSlotSession(0);
}
bool MCAuthManager::IsJavaLoggedIn() const {
return IsSlotLoggedIn(0);
}
int MCAuthManager::AddOfflineJavaAccount(const std::string& username) {
if (username.empty()) return -1;
std::string uuid = MCAuth::GenerateOfflineUuid(username);
if (uuid.empty()) return -1;
std::lock_guard<std::mutex> alock(m_accountsMutex);
// Check if this offline account already exists (by UUID)
for (int i = 0; i < (int)m_javaAccounts.size(); ++i) {
if (m_javaAccounts[i].uuid == uuid) {
m_javaAccounts[i].username = username;
// Set slot 0 to this account
{
std::lock_guard<std::mutex> lock(m_slots[0].mutex);
m_slots[0].accountIndex = i;
m_slots[0].session = SynthesizeOfflineSession(m_javaAccounts[i]);
m_slots[0].state = State::Success;
m_slots[0].cv.notify_all();
}
AUTH_LOG("AddOfflineJavaAccount: updated existing offline account idx=%d '%s'",
i, username.c_str());
return i;
}
}
JavaAccountInfo info;
info.username = username;
info.uuid = uuid;
info.tokenFile = "";
info.isOffline = true;
info.authProvider = "offline";
m_javaAccounts.push_back(std::move(info));
int idx = (int)m_javaAccounts.size() - 1;
// Set slot 0 to this account
{
std::lock_guard<std::mutex> lock(m_slots[0].mutex);
m_slots[0].accountIndex = idx;
m_slots[0].session = SynthesizeOfflineSession(m_javaAccounts[idx]);
m_slots[0].state = State::Success;
m_slots[0].cv.notify_all();
}
AUTH_LOG("AddOfflineJavaAccount: added new offline account idx=%d '%s' uuid='%s'",
idx, username.c_str(), uuid.c_str());
return idx;
}
void MCAuthManager::BeginAddJavaAccount(DeviceCodeCb onDeviceCode,
JavaCompleteCb onComplete,
int timeoutSeconds)
{
auto& s = m_slots[0];
// Invalidate any in-flight work on slot 0
uint32_t gen = ++s.generation;
// Clear stale device code
{
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
m_javaDeviceCode.clear();
m_javaDirectUri.clear();
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::WaitingForCode;
s.cv.notify_all();
}
auto freshAuth = ResetSlotAuth(s);
std::thread([this, freshAuth, onDeviceCode, onComplete, timeoutSeconds, gen]() {
try {
auto& s = m_slots[0];
MCAuth::JavaSession session;
std::string error;
bool ok = freshAuth->Login(
[&](const MCAuth::DeviceCodeInfo& dc) {
// If a newer flow started, don't overwrite its device code
if (s.generation != gen) return;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::WaitingForCode;
s.cv.notify_all();
}
{
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
m_javaDeviceCode = dc.userCode;
m_javaDirectUri = dc.directUri;
}
if (onDeviceCode) onDeviceCode(dc);
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
},
session, error, timeoutSeconds);
// ---- STALE CHECK (under mutex for correctness) ----
// If a newer operation has started, discard results silently.
{
std::lock_guard<std::mutex> lock(s.mutex);
if (s.generation != gen) {
AUTH_LOG("BeginAddJavaAccount: gen %u stale (current %u), discarding",
gen, s.generation.load());
if (onComplete) onComplete(false, {}, "cancelled");
return;
}
}
if (ok) {
std::string tokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
// Check if this account already exists (by UUID)
int existingIdx = -1;
for (int i = 0; i < (int)m_javaAccounts.size(); ++i) {
if (m_javaAccounts[i].uuid == session.uuid) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_javaAccounts[existingIdx].username = session.username;
tokenFile = m_javaAccounts[existingIdx].tokenFile;
s.accountIndex = existingIdx;
AUTH_LOG("BeginAddJavaAccount: updated existing account idx=%d '%s'",
existingIdx, session.username.c_str());
} else {
JavaAccountInfo info;
info.username = session.username;
info.uuid = session.uuid;
info.tokenFile = AllocTokenFile(session.uuid);
info.authProvider = "mojang";
tokenFile = info.tokenFile;
m_javaAccounts.push_back(std::move(info));
s.accountIndex = (int)m_javaAccounts.size() - 1;
AUTH_LOG("BeginAddJavaAccount: added new account idx=%d '%s' file='%s'",
s.accountIndex.load(), session.username.c_str(),
m_javaAccounts.back().tokenFile.c_str());
}
}
// File I/O outside all mutexes
if (!tokenFile.empty())
freshAuth->SaveTokens(tokenFile);
{
std::lock_guard<std::mutex> lock(s.mutex);
s.session = session;
s.lastError.clear();
s.state = State::Success;
s.cv.notify_all();
}
SaveJavaAccountIndex();
} else {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = error;
s.state = State::Failed;
s.cv.notify_all();
}
// If we had an active account, reload it into the current auth engine
std::string reloadFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size())
reloadFile = m_javaAccounts[s.accountIndex].tokenFile;
}
if (!reloadFile.empty())
freshAuth->LoadTokens(reloadFile);
}
if (onComplete) onComplete(ok, session, error);
} catch (const std::exception& ex) {
AUTH_LOG("BeginAddJavaAccount: uncaught exception: %s", ex.what());
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = std::string("Auth network error: ") + ex.what();
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, s.lastError);
} catch (...) {
AUTH_LOG("BeginAddJavaAccount: unknown uncaught exception");
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = "Unknown auth error";
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, "Unknown auth error");
}
}).detach();
}
void MCAuthManager::BeginAddElybyAccount(const std::string& username, const std::string& password,
ElybyCompleteCb onComplete, Elyby2FACb on2FA)
{
auto& s = m_slots[0];
uint32_t gen = ++s.generation;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Authenticating;
s.cv.notify_all();
}
std::thread([this, username, password, onComplete, on2FA, gen]() {
try {
auto& s = m_slots[0];
MCAuth::ElybyTokens tokens;
std::string error;
bool ok = MCAuth::ElybyLogin(username, password, tokens, error);
if (!ok && error == "elyby_2fa_required") {
// Signal caller that 2FA is needed
{
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::WaitingForCode; // repurpose: waiting for 2FA input
s.cv.notify_all();
}
if (on2FA) on2FA();
if (onComplete) onComplete(false, {}, "elyby_2fa_required");
return;
}
{
std::lock_guard<std::mutex> lock(s.mutex);
if (s.generation != gen) {
if (onComplete) onComplete(false, {}, "cancelled");
return;
}
}
if (ok) {
std::string dashedUuid = MCAuth::DashUuid(tokens.uuid);
std::string tokenFile;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
int existingIdx = -1;
for (int i = 0; i < (int)m_javaAccounts.size(); ++i) {
if (m_javaAccounts[i].uuid == dashedUuid) {
existingIdx = i;
break;
}
}
if (existingIdx >= 0) {
m_javaAccounts[existingIdx].username = tokens.username;
m_javaAccounts[existingIdx].authProvider = "elyby";
tokenFile = m_javaAccounts[existingIdx].tokenFile;
s.accountIndex = existingIdx;
} else {
JavaAccountInfo info;
info.username = tokens.username;
info.uuid = dashedUuid;
info.tokenFile = AllocTokenFile(dashedUuid);
info.isOffline = false;
info.authProvider = "elyby";
tokenFile = info.tokenFile;
m_javaAccounts.push_back(std::move(info));
s.accountIndex = (int)m_javaAccounts.size() - 1;
}
}
if (!tokenFile.empty())
MCAuth::ElybySaveTokens(tokenFile, tokens);
MCAuth::JavaSession session;
session.username = tokens.username;
session.uuid = dashedUuid;
session.accessToken = tokens.accessToken;
session.expireMs = 0;
{
std::lock_guard<std::mutex> lock(s.mutex);
s.elybyTokens = tokens;
s.session = session;
s.lastError.clear();
s.state = State::Success;
s.cv.notify_all();
}
SaveJavaAccountIndex();
if (onComplete) onComplete(true, session, "");
} else {
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = error;
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, error);
}
} catch (const std::exception& ex) {
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = std::string("Elyby auth error: ") + ex.what();
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, s.lastError);
} catch (...) {
auto& s = m_slots[0];
{
std::lock_guard<std::mutex> lock(s.mutex);
s.lastError = "Unknown elyby auth error";
s.state = State::Failed;
s.cv.notify_all();
}
if (onComplete) onComplete(false, {}, "Unknown elyby auth error");
}
}).detach();
}
bool MCAuthManager::RemoveJavaAccount(int index) {
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (index < 0 || index >= (int)m_javaAccounts.size()) return false;
AUTH_LOG("RemoveJavaAccount: removing index %d ('%s')",
index, m_javaAccounts[index].username.c_str());
std::remove(m_javaAccounts[index].tokenFile.c_str());
m_javaAccounts.erase(m_javaAccounts.begin() + index);
// Adjust all slots' account indices
for (int i = 0; i < XUSER_MAX_COUNT; ++i) {
auto& s = m_slots[i];
if (s.accountIndex == index) {
// This slot was using the removed account
if (m_javaAccounts.empty()) {
s.accountIndex = -1;
std::lock_guard<std::mutex> lock(s.mutex);
s.session = {};
s.state = State::Idle;
s.cv.notify_all();
} else {
s.accountIndex = 0;
// Caller should call SetAccountForSlot to reload
}
} else if (s.accountIndex > index) {
--s.accountIndex;
}
}
return true; // caller must call SaveJavaAccountIndex() after
}
void MCAuthManager::TryRestoreActiveJavaAccount() {
AUTH_LOG("TryRestoreActiveJavaAccount called (already attempted=%d)",
(int)m_javaRestoreAttempted);
if (m_javaRestoreAttempted) return;
m_javaRestoreAttempted = true;
if (!LoadJavaAccountIndex()) {
// Try legacy single-file migration
auto& s = m_slots[0];
if (s.auth->LoadTokens("java_auth.json")) {
AUTH_LOG("TryRestoreActiveJavaAccount: migrating legacy java_auth.json");
std::lock_guard<std::mutex> alock(m_accountsMutex);
JavaAccountInfo info;
info.username = "";
info.uuid = "";
info.tokenFile = "legacy_migrated.json";
s.auth->SaveTokens(info.tokenFile);
m_javaAccounts.push_back(std::move(info));
s.accountIndex = 0;
std::remove("java_auth.json");
} else {
AUTH_LOG("TryRestoreActiveJavaAccount: no accounts found");
return;
}
}
auto& s = m_slots[0];
int activeIdx = s.accountIndex;
std::string tokenFile;
bool isOffline = false;
bool isElyby = false;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
if (activeIdx < 0 || activeIdx >= (int)m_javaAccounts.size()) {
AUTH_LOG("TryRestoreActiveJavaAccount: no active account");
return;
}
isOffline = m_javaAccounts[activeIdx].isOffline;
isElyby = (m_javaAccounts[activeIdx].authProvider == "elyby");
tokenFile = m_javaAccounts[activeIdx].tokenFile;
}
if (isElyby) {
AUTH_LOG("TryRestoreActiveJavaAccount: elyby account %d, loading tokens from '%s'",
activeIdx, tokenFile.c_str());
MCAuth::ElybyTokens elyTokens;
if (!MCAuth::ElybyLoadTokens(tokenFile, elyTokens)) {
AUTH_LOG("TryRestoreActiveJavaAccount: ElybyLoadTokens failed for '%s'", tokenFile.c_str());
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.lastError = "Failed to load ely.by token file: " + tokenFile;
s.cv.notify_all();
return;
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.elybyTokens = elyTokens;
s.session.username = elyTokens.username;
s.session.uuid = MCAuth::DashUuid(elyTokens.uuid);
s.session.accessToken = elyTokens.accessToken;
s.session.expireMs = 0;
s.state = State::Success;
s.cv.notify_all();
}
// Refresh in background
RefreshSlot(0);
return;
}
if (isOffline) {
AUTH_LOG("TryRestoreActiveJavaAccount: offline account %d, no refresh needed", activeIdx);
MCAuth::JavaSession offlineSession;
{
std::lock_guard<std::mutex> alock(m_accountsMutex);
offlineSession = SynthesizeOfflineSession(m_javaAccounts[activeIdx]);
}
{
std::lock_guard<std::mutex> lock(s.mutex);
s.session = offlineSession;
s.state = State::Success;
s.cv.notify_all();
}
return;
}
AUTH_LOG("TryRestoreActiveJavaAccount: loading tokenFile='%s' for account %d",
tokenFile.c_str(), activeIdx);
if (!s.auth->LoadTokens(tokenFile)) {
AUTH_LOG("TryRestoreActiveJavaAccount: LoadTokens failed for '%s'", tokenFile.c_str());
std::lock_guard<std::mutex> lock(s.mutex);
s.state = State::Failed;
s.lastError = "Failed to load token file: " + tokenFile;
s.cv.notify_all();
return;
}
RefreshSlot(0);
}
std::string MCAuthManager::GetJavaDeviceCode() const {
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
return m_javaDeviceCode;
}
std::string MCAuthManager::GetJavaDirectUri() const {
std::lock_guard<std::mutex> lock(m_deviceCodeMutex);
return m_javaDirectUri;
}
std::string MCAuthManager::GetLastError() const {
auto& s = m_slots[0];
std::lock_guard<std::mutex> lock(s.mutex);
return s.lastError;
}

View file

@ -0,0 +1,476 @@
/*
* MCAuthSession.cpp UUID utilities and Mojang session verification
*
* UUID v3 generation uses MD5 with the "OfflinePlayer:" namespace,
* matching vanilla Minecraft's offline UUID derivation.
*
* Session verification:
* - JoinServer() client calls before connecting (POST /session/minecraft/join)
* - HasJoined() server verifies after receiving auth response (GET /session/minecraft/hasJoined)
*/
#include "../include/MCAuth.h"
#include "MCAuthCrypto.h"
#include "MCAuthHttp.h"
#include "MCAuthInternal.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#include <wincodec.h>
#include <sstream>
#include <cstdint>
#include <cstring>
#include <vector>
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "windowscodecs.lib")
#pragma comment(lib, "ole32.lib")
namespace MCAuth {
using namespace mcauth;
std::string MakeSkinKey(const std::string& uuid) {
return "mojang_skin_" + UndashUuid(uuid) + ".png";
}
std::string UndashUuid(const std::string& dashed) {
std::string out;
out.reserve(32);
for (char c : dashed) {
if (c != '-') out += c;
}
return out;
}
std::string DashUuid(const std::string& u) {
if (u.size() != 32) return u;
// 8-4-4-4-12
return u.substr(0, 8) + "-" + u.substr(8, 4) + "-" + u.substr(12, 4)
+ "-" + u.substr(16, 4) + "-" + u.substr(20);
}
std::string GenerateOfflineUuid(const std::string& username) {
std::string input = "OfflinePlayer:" + username;
auto md5 = ComputeMD5(input.data(), input.size());
if (md5.size() < 16) return "";
uint8_t hash[16];
memcpy(hash, md5.data(), 16);
// Set version to 3 (byte 6, high nibble)
hash[6] = (hash[6] & 0x0F) | 0x30;
// Set variant to RFC 4122 (byte 8, high 2 bits = 10)
hash[8] = (hash[8] & 0x3F) | 0x80;
// Convert to hex string
char hex[33];
for (int i = 0; i < 16; i++)
snprintf(hex + i * 2, 3, "%02x", hash[i]);
hex[32] = '\0';
return DashUuid(std::string(hex, 32));
}
static uint8_t HexNibble(char c) {
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
if (c >= 'a' && c <= 'f') return (uint8_t)(c - 'a' + 10);
if (c >= 'A' && c <= 'F') return (uint8_t)(c - 'A' + 10);
return 0;
}
Uuid128 ParseUuid128(const std::string& dashed) {
std::string hex = UndashUuid(dashed);
if (hex.size() != 32) return {};
Uuid128 result;
result.hi = 0;
result.lo = 0;
for (int i = 0; i < 16; i++) {
uint8_t byte = (HexNibble(hex[i * 2]) << 4) | HexNibble(hex[i * 2 + 1]);
if (i < 8)
result.hi = (result.hi << 8) | byte;
else
result.lo = (result.lo << 8) | byte;
}
return result;
}
// Classify an HTTP status code into an AuthErrorCode
static AuthErrorCode ClassifyHttpStatus(int status) {
if (status == 401 || status == 403) return AuthErrorCode::InvalidCredentials;
if (status == 429) return AuthErrorCode::RateLimited;
if (status >= 500 && status < 600) return AuthErrorCode::ServerUnavailable;
return AuthErrorCode::HttpError;
}
static bool JoinServerImpl(const std::string& url,
const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
const std::string& errorPrefix,
std::string& error)
{
std::string body = "{\"accessToken\":\"" + JsonEscape(accessToken)
+ "\",\"selectedProfile\":\"" + JsonEscape(undashedUuid)
+ "\",\"serverId\":\"" + JsonEscape(serverId) + "\"}";
try {
auto resp = HttpPost(url, body, "application/json");
if (resp.statusCode == 204 || resp.statusCode == 200)
return true;
error = errorPrefix + " HTTP " + std::to_string(resp.statusCode);
if (!resp.body.empty()) {
std::string msg = JsonGetString(resp.body, "errorMessage");
if (!msg.empty()) error += ": " + msg;
else error += " body: " + resp.body.substr(0, 200);
}
} catch (const std::exception& e) {
error = errorPrefix + " network error: " + e.what();
}
return false;
}
bool JoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error)
{
return JoinServerImpl(
"https://sessionserver.mojang.com/session/minecraft/join",
accessToken, undashedUuid, serverId, "JoinServer", error);
}
// Extract texture URLs from HasJoined response body.
// The "properties" array contains a "textures" entry whose value is base64-encoded JSON.
static void ParseTextureProperties(const std::string& body, std::string& skinUrl, std::string& capeUrl) {
// Find the "properties" array, then find the entry with name "textures"
size_t propsPos = body.find("\"properties\"");
if (propsPos == std::string::npos) return;
// Find "textures" name entry
size_t texNamePos = body.find("\"textures\"", propsPos);
if (texNamePos == std::string::npos) return;
// Find the "value" field after "textures"
std::string b64Value = JsonGetString(body.substr(texNamePos), "value");
if (b64Value.empty()) return;
std::string decoded = Base64DecodeStr(b64Value);
if (decoded.empty()) return;
// Parse the decoded JSON for SKIN and CAPE urls
// Structure: {"textures":{"SKIN":{"url":"...","metadata":{...}},"CAPE":{"url":"..."}}}
// Use JsonRawValue for proper brace-depth tracking (SKIN may contain nested "metadata")
std::string skinObj = JsonRawValue(decoded, "SKIN");
if (!skinObj.empty())
skinUrl = JsonGetString(skinObj, "url");
std::string capeObj = JsonRawValue(decoded, "CAPE");
if (!capeObj.empty())
capeUrl = JsonGetString(capeObj, "url");
}
static HasJoinedResult HasJoinedImpl(const std::string& baseUrl,
const std::string& username,
const std::string& serverId,
const std::string& errorPrefix,
std::string& error)
{
std::string url = baseUrl + "?username="
+ UrlEncode(username) + "&serverId=" + UrlEncode(serverId);
HasJoinedResult result;
try {
auto resp = HttpGet(url);
if (resp.statusCode == 200 && !resp.body.empty()) {
result.success = true;
result.uuid = JsonGetString(resp.body, "id");
result.username = JsonGetString(resp.body, "name");
ParseTextureProperties(resp.body, result.skinUrl, result.capeUrl);
return result;
}
result.success = false;
if (resp.statusCode == 204) {
error = "Player has not joined (session not found)";
result.error = { AuthErrorCode::InvalidCredentials, error, 204 };
} else {
error = errorPrefix + " HTTP " + std::to_string(resp.statusCode);
result.error = { ClassifyHttpStatus(resp.statusCode), error, resp.statusCode };
}
} catch (const std::exception& e) {
error = errorPrefix + " network error: " + e.what();
result.success = false;
result.error = { AuthErrorCode::NetworkError, error, 0 };
}
return result;
}
HasJoinedResult HasJoined(const std::string& username,
const std::string& serverId,
std::string& error)
{
return HasJoinedImpl(
"https://sessionserver.mojang.com/session/minecraft/hasJoined",
username, serverId, "HasJoined", error);
}
// CropSkinTo64x32 — convert 64x64 Java Edition skin to 64x32 LCE format.
// Uses WIC for PNG decode/encode. Returns original data unchanged if not 64x64.
static std::vector<uint8_t> CropSkinTo64x32(const std::vector<uint8_t>& pngData) {
// Guard against exceptions in WIC/COM — this runs in a detached thread
// (PendingConnection auth verification) where unhandled exceptions crash the process.
try {
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
bool needUninit = SUCCEEDED(hr);
IWICImagingFactory* factory = nullptr;
IWICStream* stream = nullptr;
IWICBitmapDecoder* decoder = nullptr;
IWICBitmapFrameDecode*frame = nullptr;
IWICFormatConverter* converter = nullptr;
IWICStream* outStream = nullptr;
IStream* memStream = nullptr;
IWICBitmapEncoder* encoder = nullptr;
IWICBitmapFrameEncode*outFrame = nullptr;
// Lambda-based cleanup — captures pointers by reference so it always
// releases whatever was allocated up to the point of the call.
auto cleanup = [&]() {
if (outFrame) outFrame->Release();
if (encoder) encoder->Release();
if (outStream) outStream->Release();
if (memStream) memStream->Release();
if (converter) converter->Release();
if (frame) frame->Release();
if (decoder) decoder->Release();
if (stream) stream->Release();
if (factory) factory->Release();
if (needUninit) CoUninitialize();
};
hr = CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory));
if (FAILED(hr)) { cleanup(); return pngData; }
// Macro for concise HRESULT checking — on failure, clean up and return
// the original PNG (graceful fallback: skin displayed uncropped).
#define WIC_CHECK(expr) do { hr = (expr); if (FAILED(hr)) { cleanup(); return pngData; } } while(0)
// Decode the PNG from memory
WIC_CHECK(factory->CreateStream(&stream));
WIC_CHECK(stream->InitializeFromMemory((BYTE*)pngData.data(), (DWORD)pngData.size()));
WIC_CHECK(factory->CreateDecoderFromStream(stream, nullptr, WICDecodeMetadataCacheOnDemand, &decoder));
WIC_CHECK(decoder->GetFrame(0, &frame));
UINT width = 0, height = 0;
WIC_CHECK(frame->GetSize(&width, &height));
// Only crop if the skin is 64x64 (Java Edition format)
if (width != 64 || height != 64) { cleanup(); return pngData; }
// Convert to 32bpp BGRA
WIC_CHECK(factory->CreateFormatConverter(&converter));
WIC_CHECK(converter->Initialize(frame, GUID_WICPixelFormat32bppBGRA,
WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom));
// Copy the top 32 rows (64x32 crop)
const UINT cropW = 64, cropH = 32;
const UINT stride = cropW * 4;
std::vector<BYTE> pixels(stride * cropH);
WICRect cropRect = { 0, 0, (INT)cropW, (INT)cropH };
WIC_CHECK(converter->CopyPixels(&cropRect, stride, (UINT)pixels.size(), pixels.data()));
// Encode the cropped image back to PNG
WIC_CHECK(factory->CreateStream(&outStream));
hr = CreateStreamOnHGlobal(nullptr, TRUE, &memStream);
if (FAILED(hr)) { cleanup(); return pngData; }
WIC_CHECK(outStream->InitializeFromIStream(memStream));
WIC_CHECK(factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder));
WIC_CHECK(encoder->Initialize(outStream, WICBitmapEncoderNoCache));
WIC_CHECK(encoder->CreateNewFrame(&outFrame, nullptr));
WIC_CHECK(outFrame->Initialize(nullptr));
WIC_CHECK(outFrame->SetSize(cropW, cropH));
WICPixelFormatGUID pixFmt = GUID_WICPixelFormat32bppBGRA;
WIC_CHECK(outFrame->SetPixelFormat(&pixFmt));
WIC_CHECK(outFrame->WritePixels(cropH, stride, (UINT)pixels.size(), pixels.data()));
WIC_CHECK(outFrame->Commit());
WIC_CHECK(encoder->Commit());
// Read the encoded PNG from the IStream
STATSTG stat = {};
WIC_CHECK(memStream->Stat(&stat, STATFLAG_NONAME));
ULONG pngSize = (ULONG)stat.cbSize.QuadPart;
#undef WIC_CHECK
std::vector<uint8_t> result(pngSize);
LARGE_INTEGER zero = {};
memStream->Seek(zero, STREAM_SEEK_SET, nullptr);
memStream->Read(result.data(), pngSize, nullptr);
cleanup();
return result;
} catch (...) {
// WIC/COM failure — return original data rather than crashing the detached thread
return pngData;
}
}
bool ValidateSkinPng(const uint8_t* data, size_t size) {
// PNG magic: 89 50 4E 47 0D 0A 1A 0A
static const uint8_t kPngMagic[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
if (data == nullptr || size < 24) // PNG header + IHDR minimum
return false;
if (size > kMaxSkinBytes)
return false;
if (memcmp(data, kPngMagic, 8) != 0)
return false;
// Read IHDR dimensions (bytes 16-23 in a PNG: 4 bytes width + 4 bytes height, big-endian)
uint32_t width = ((uint32_t)data[16] << 24) | ((uint32_t)data[17] << 16) |
((uint32_t)data[18] << 8) | (uint32_t)data[19];
uint32_t height = ((uint32_t)data[20] << 24) | ((uint32_t)data[21] << 16) |
((uint32_t)data[22] << 8) | (uint32_t)data[23];
// Minecraft skins are 64x32 (legacy) or 64x64 (modern)
if (width != 64)
return false;
if (height != 32 && height != 64)
return false;
return true;
}
std::vector<uint8_t> FetchSkinPng(const std::string& url, std::string& error) {
if (url.empty()) {
error = "Empty skin URL";
return {};
}
try {
auto resp = HttpGet(url);
if (resp.statusCode == 200 && !resp.body.empty()) {
auto rawPng = std::vector<uint8_t>(resp.body.begin(), resp.body.end());
// Validate before processing — reject non-PNG or wrong dimensions
if (!ValidateSkinPng(rawPng.data(), rawPng.size())) {
error = "FetchSkinPng: invalid PNG data (" + std::to_string(rawPng.size()) + " bytes)";
return {};
}
// Convert 64x64 Java skin to 64x32 LCE format
return CropSkinTo64x32(rawPng);
}
error = "FetchSkinPng HTTP " + std::to_string(resp.statusCode);
} catch (const std::exception& e) {
error = std::string("FetchSkinPng exception: ") + e.what();
}
return {};
}
std::vector<uint8_t> FetchSkinPngRaw(const std::string& url, std::string& error) {
if (url.empty()) {
error = "Empty skin URL";
return {};
}
try {
auto resp = HttpGet(url);
if (resp.statusCode == 200 && !resp.body.empty()) {
auto rawPng = std::vector<uint8_t>(resp.body.begin(), resp.body.end());
if (!ValidateSkinPng(rawPng.data(), rawPng.size())) {
error = "FetchSkinPngRaw: invalid PNG data";
return {};
}
return rawPng; // Return original RGBA PNG — game's LoadTextureData handles format
}
error = "FetchSkinPngRaw HTTP " + std::to_string(resp.statusCode);
} catch (const std::exception& e) {
error = std::string("FetchSkinPngRaw exception: ") + e.what();
}
return {};
}
static std::string FetchProfileSkinUrlImpl(const std::string& baseUrl,
const std::string& uuid,
const std::string& errorPrefix,
const std::string& noSkinMsg,
std::string& error)
{
std::string undashed = UndashUuid(uuid);
if (undashed.empty()) {
error = "Empty UUID";
return "";
}
std::string url = baseUrl + undashed;
try {
auto resp = HttpGet(url);
if (resp.statusCode != 200 || resp.body.empty()) {
error = errorPrefix + " HTTP " + std::to_string(resp.statusCode);
return "";
}
std::string skinUrl, capeUrl;
ParseTextureProperties(resp.body, skinUrl, capeUrl);
if (skinUrl.empty())
error = noSkinMsg;
return skinUrl;
} catch (const std::exception& e) {
error = errorPrefix + " exception: " + e.what();
return "";
}
}
std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error) {
return FetchProfileSkinUrlImpl(
"https://sessionserver.mojang.com/session/minecraft/profile/",
uuid, "Profile", "No skin in profile", error);
}
// Ely.by session functions — same Yggdrasil protocol, different base URLs
std::string MakeElybySkinKey(const std::string& uuid) {
return "elyby_skin_" + UndashUuid(uuid) + ".png";
}
bool ElybyJoinServer(const std::string& accessToken,
const std::string& undashedUuid,
const std::string& serverId,
std::string& error)
{
return JoinServerImpl(
"https://authserver.ely.by/session/join",
accessToken, undashedUuid, serverId, "ElybyJoinServer", error);
}
HasJoinedResult ElybyHasJoined(const std::string& username,
const std::string& serverId,
std::string& error)
{
return HasJoinedImpl(
"https://authserver.ely.by/session/hasJoined",
username, serverId, "ElybyHasJoined", error);
}
std::string ElybyFetchProfileSkinUrl(const std::string& uuid, std::string& error) {
return FetchProfileSkinUrlImpl(
"https://authserver.ely.by/session/profile/",
uuid, "ElybyProfile", "No skin in ely.by profile", error);
}
} // namespace MCAuth

View file

@ -51,6 +51,7 @@ set_target_properties(Minecraft.Client PROPERTIES
target_link_libraries(Minecraft.Client PRIVATE target_link_libraries(Minecraft.Client PRIVATE
Minecraft.World Minecraft.World
MCAuth
d3d11 d3d11
d3dcompiler d3dcompiler
XInput9_1_0 XInput9_1_0

View file

@ -28,6 +28,11 @@
#include "..\Minecraft.World\net.minecraft.world.level.tile.h" #include "..\Minecraft.World\net.minecraft.world.level.tile.h"
#include "..\Minecraft.World\net.minecraft.world.inventory.h" #include "..\Minecraft.World\net.minecraft.world.inventory.h"
#include "..\Minecraft.World\net.minecraft.world.h" #include "..\Minecraft.World\net.minecraft.world.h"
#ifdef _WINDOWS64
#include "..\..\MCAuth\include\MCAuth.h"
#include "..\..\MCAuth\include\MCAuthManager.h"
#include "..\Minecraft.World\GameUUID.h"
#endif
#include "..\Minecraft.World\net.minecraft.world.level.saveddata.h" #include "..\Minecraft.World\net.minecraft.world.level.saveddata.h"
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h" #include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
#include "..\Minecraft.World\net.minecraft.world.effect.h" #include "..\Minecraft.World\net.minecraft.world.effect.h"
@ -365,7 +370,14 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> packet)
} }
minecraft->player->setPlayerIndex( packet->m_playerIndex ); minecraft->player->setPlayerIndex( packet->m_playerIndex );
minecraft->player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) ); if (!m_mojangSkinUrl.empty())
{
minecraft->player->customTextureUrl = m_mojangSkinUrl;
}
else
{
minecraft->player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) );
}
minecraft->player->setCustomCape( app.GetPlayerCapeId(m_userIndex) ); minecraft->player->setCustomCape( app.GetPlayerCapeId(m_userIndex) );
@ -441,7 +453,14 @@ void ClientConnection::handleLogin(shared_ptr<LoginPacket> packet)
player->entityId = packet->clientVersion; player->entityId = packet->clientVersion;
player->setPlayerIndex( packet->m_playerIndex ); player->setPlayerIndex( packet->m_playerIndex );
player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) ); if (!m_mojangSkinUrl.empty())
{
player->customTextureUrl = m_mojangSkinUrl;
}
else
{
player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) );
}
player->setCustomCape( app.GetPlayerCapeId(m_userIndex) ); player->setCustomCape( app.GetPlayerCapeId(m_userIndex) );
@ -840,22 +859,42 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
return; return;
} }
} }
#endif
/*#ifdef _WINDOWS64 // Additional check: compare against MCAuth session UUIDs for all slots.
// On Windows64 all XUIDs are INVALID_XUID so the XUID check above never fires. // This catches the case where the AddPlayerPacket arrives BEFORE the local player
// packet->m_playerIndex is the server-assigned sequential index (set via LoginPacket), // entity is created (race condition during splitscreen join), preventing ghost entities.
// NOT the controller slot — so we must scan all local player slots and match by
// their stored server index rather than using it directly as an array subscript.
for(unsigned int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
{ {
if(minecraft->localplayers[idx] != nullptr && auto& mgr = MCAuthManager::Get();
minecraft->localplayers[idx]->getPlayerIndex() == packet->m_playerIndex) for (int slot = 0; slot < XUSER_MAX_COUNT; ++slot)
{ {
app.DebugPrintf("AddPlayerPacket received for local player (controller %d, server index %d), skipping RemotePlayer creation\n", idx, packet->m_playerIndex); if (!mgr.IsSlotLoggedIn(slot)) continue;
return; auto session = mgr.GetSlotSession(slot);
if (session.uuid.empty()) continue;
PlayerUID authXuid = GameUUID::fromDashed(session.uuid);
if (authXuid != INVALID_XUID && authXuid == packet->xuid)
{
app.DebugPrintf("AddPlayerPacket for local auth slot %d ('%s'), skipping RemotePlayer creation\n",
slot, session.username.c_str());
return;
}
} }
} }
#endif*/
// Also check by local player entity XUID (set during createExtraLocalPlayer)
for (unsigned int idx = 0; idx < XUSER_MAX_COUNT; ++idx)
{
if (minecraft->localplayers[idx] != nullptr)
{
PlayerUID localXuid = minecraft->localplayers[idx]->getXuid();
if (localXuid != INVALID_XUID && localXuid == packet->xuid)
{
app.DebugPrintf("AddPlayerPacket matched local player %ls by XUID, skipping\n",
minecraft->localplayers[idx]->name.c_str());
return;
}
}
}
#endif
double x = packet->x / 32.0; double x = packet->x / 32.0;
double y = packet->y / 32.0; double y = packet->y / 32.0;
@ -884,21 +923,8 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
{ {
IQNetPlayer* matchedQNetPlayer = nullptr; IQNetPlayer* matchedQNetPlayer = nullptr;
PlayerUID pktXuid = player->getXuid(); 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 != nullptr)
{
NetworkPlayerXbox* npx = (NetworkPlayerXbox*)np;
matchedQNetPlayer = npx->GetQNetPlayer();
}
}
// Current Win64 path: identify QNet player by name and attach packet XUID. // Identify QNet player by name and attach packet XUID.
if (matchedQNetPlayer == nullptr)
{ {
for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; ++i) for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; ++i)
{ {
@ -919,8 +945,7 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
if (matchedQNetPlayer != nullptr) if (matchedQNetPlayer != nullptr)
{ {
// Store packet-authoritative XUID on this network slot so later lookups by XUID // Store packet-authoritative UUID on this network slot so later lookups work.
// (e.g. remove player, display mapping) work for both legacy and uid.dat clients.
matchedQNetPlayer->m_resolvedXuid = pktXuid; matchedQNetPlayer->m_resolvedXuid = pktXuid;
if (matchedQNetPlayer->m_gamertag[0] == 0) if (matchedQNetPlayer->m_gamertag[0] == 0)
{ {
@ -997,6 +1022,13 @@ void ClientConnection::handleAddPlayer(shared_ptr<AddPlayerPacket> packet)
player->getEntityData()->assignValues(unpackedData); player->getEntityData()->assignValues(unpackedData);
} }
// Retry any SetEntityLinkPackets that were waiting for this player to be added.
// Critical for splitscreen: when player 2 joins, the server may broadcast a
// SetEntityLinkPacket for player 1 (e.g. riding a horse) before player 2's
// connection has received the AddPlayerPacket for player 1. Without this call,
// the deferred packet would never be resolved and the riding state would be lost
// (the original code only called this from handleAddEntity, not handleAddPlayer).
checkDeferredEntityLinkPackets(packet->id);
} }
void ClientConnection::handleTeleportEntity(shared_ptr<TeleportEntityPacket> packet) void ClientConnection::handleTeleportEntity(shared_ptr<TeleportEntityPacket> packet)
@ -1403,6 +1435,7 @@ void ClientConnection::handleTileUpdate(shared_ptr<TileUpdatePacket> packet)
void ClientConnection::handleDisconnect(shared_ptr<DisconnectPacket> packet) void ClientConnection::handleDisconnect(shared_ptr<DisconnectPacket> packet)
{ {
app.DebugPrintf("[Auth-Client] Received DisconnectPacket: reason=%d\n", (int)packet->reason);
connection->close(DisconnectPacket::eDisconnect_Kicked); connection->close(DisconnectPacket::eDisconnect_Kicked);
done = true; done = true;
@ -1418,6 +1451,7 @@ void ClientConnection::handleDisconnect(shared_ptr<DisconnectPacket> packet)
void ClientConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects) void ClientConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects)
{ {
app.DebugPrintf("[Auth-Client] onDisconnect called: reason=%d, done=%d\n", (int)reason, (int)done);
if (done) return; if (done) return;
done = true; done = true;
@ -2417,39 +2451,18 @@ void ClientConnection::handlePreLogin(shared_ptr<PreLoginPacket> packet)
{ {
Minecraft::GetInstance()->progressRenderer->progressStagePercentage((eCCPreLoginReceived * 100)/ (eCCConnected)); Minecraft::GetInstance()->progressRenderer->progressStagePercentage((eCCPreLoginReceived * 100)/ (eCCConnected));
} }
// need to use the XUID here // Cache ugcPlayersVersion for later LoginPacket
PlayerUID offlineXUID = INVALID_XUID; m_cachedUgcPlayersVersion = packet->m_ugcPlayersVersion;
PlayerUID onlineXUID = INVALID_XUID;
if( ProfileManager.IsSignedInLive(m_userIndex) )
{
// Guest don't have an offline XUID as they cannot play offline, so use their online one
ProfileManager.GetXUID(m_userIndex,&onlineXUID,true);
}
#ifdef __PSVITA__
if(CGameNetworkManager::usingAdhocMode() && onlineXUID.getOnlineID()[0] == 0)
{
// player doesn't have an online UID, set it from the player name
onlineXUID.setForAdhoc();
}
#endif
// On PS3, all non-signed in players (even guests) can get a useful offlineXUID // If auth already completed (e.g., UGC version mismatch re-send from server),
#if !(defined __PS3__ || defined _DURANGO ) // re-send LoginPacket immediately instead of waiting for AuthSchemePacket.
if( !ProfileManager.IsGuest( m_userIndex ) ) if (!m_assignedUuid.empty())
#endif
{ {
// All other players we use their offline XUID so that they can play the game offline app.DebugPrintf("[Auth-Client] Auth already completed, re-sending LoginPacket (UGC resync)\n");
ProfileManager.GetXUID(m_userIndex,&offlineXUID,false); sendLoginPacketAfterAuth();
} return;
BOOL allAllowed, friendsAllowed;
ProfileManager.AllowedPlayerCreatedContent(m_userIndex,true,&allAllowed,&friendsAllowed);
send(std::make_shared<LoginPacket>(minecraft->user->name, SharedConstants::NETWORK_PROTOCOL_VERSION, offlineXUID, onlineXUID, (allAllowed != TRUE && friendsAllowed == TRUE),
packet->m_ugcPlayersVersion, app.GetPlayerSkinId(m_userIndex), app.GetPlayerCapeId(m_userIndex), ProfileManager.IsGuest(m_userIndex)));
if(!g_NetworkManager.IsHost() )
{
Minecraft::GetInstance()->progressRenderer->progressStagePercentage((eCCLoginSent * 100)/ (eCCConnected));
} }
// LoginPacket sent after auth completes in sendLoginPacketAfterAuth()
} }
#else #else
// 4J - removed // 4J - removed
@ -2486,6 +2499,261 @@ void ClientConnection::close()
connection->close(DisconnectPacket::eDisconnect_Closed); connection->close(DisconnectPacket::eDisconnect_Closed);
} }
void ClientConnection::disconnectWithReason(DisconnectPacket::eDisconnectReason reason)
{
if (done) return;
connection->close(reason);
done = true;
Minecraft *pMinecraft = Minecraft::GetInstance();
pMinecraft->connectionDisconnected(m_userIndex, reason);
app.SetDisconnectReason(reason);
app.SetAction(m_userIndex, eAppAction_ExitWorld, (void *)TRUE);
}
void ClientConnection::handleAuthScheme(shared_ptr<AuthSchemePacket> packet)
{
#ifdef _WINDOWS64
auto& mgr = MCAuthManager::Get();
// Safety net: primary gate is in StartGameFromSave; this 10s wait should rarely trigger
if (mgr.GetSlotState(m_userIndex) == MCAuthManager::State::Authenticating)
{
app.DebugPrintf("[Auth-Client] Token refresh still in progress, waiting (max 10s)...\n");
mgr.WaitForSlotReady(m_userIndex, 10000);
app.DebugPrintf("[Auth-Client] Auth settled, state=%d\n", (int)mgr.GetSlotState(m_userIndex));
}
auto session = mgr.GetSlotSession(m_userIndex);
bool isLoggedIn = mgr.IsSlotLoggedIn(m_userIndex);
bool hasToken = !session.accessToken.empty();
bool tokenExpired = mgr.IsTokenExpiringSoon(m_userIndex);
bool hasOnlineAccount = isLoggedIn && hasToken && !tokenExpired;
string serverId(packet->serverId.begin(), packet->serverId.end());
app.DebugPrintf("[Auth-Client] Received AuthSchemePacket: serverId='%s'\n", serverId.c_str());
app.DebugPrintf("[Auth-Client] Session: username='%s', uuid='%s'\n",
session.username.c_str(), session.uuid.c_str());
bool serverHasMojang = false, serverHasOffline = false, serverHasElyby = false;
for (auto& s : packet->schemes)
{
if (s == L"mojang") serverHasMojang = true;
if (s == L"offline") serverHasOffline = true;
if (s == L"elyby") serverHasElyby = true;
}
// Determine account's auth provider
std::string provider = "mojang";
{
int acctIdx = mgr.GetSlot(m_userIndex).accountIndex.load();
if (acctIdx >= 0) {
auto accounts = mgr.GetJavaAccounts();
if (acctIdx < (int)accounts.size())
provider = accounts[acctIdx].authProvider;
}
}
app.DebugPrintf("[Auth-Client] Server offers: mojang=%d, offline=%d, elyby=%d (account provider=%s)\n",
(int)serverHasMojang, (int)serverHasOffline, (int)serverHasElyby, provider.c_str());
// Block connection if no account is configured at all (empty username/uuid).
// The player should sign in or create an offline account first.
if (session.username.empty() || session.uuid.empty())
{
app.DebugPrintf("[Auth-Client] REJECTED: No account configured (username/uuid empty). Player must sign in first.\n");
disconnectWithReason(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
wstring wUuid(session.uuid.begin(), session.uuid.end());
wstring wName(session.username.begin(), session.username.end());
if (hasOnlineAccount && provider == "mojang" && serverHasMojang)
{
app.DebugPrintf("[Auth-Client] Choosing 'mojang' auth scheme\n");
string token = session.accessToken;
if (token.size() > 7 && token.substr(0, 7) == "Bearer ") token = token.substr(7);
string uuid = MCAuth::UndashUuid(session.uuid);
app.DebugPrintf("[Auth-Client] Calling JoinServer for uuid='%s'...\n", uuid.c_str());
string error;
bool ok = MCAuth::JoinServer(token, uuid, serverId, error);
if (!ok)
{
app.DebugPrintf("[Auth-Client] JoinServer FAILED: %s\n", error.c_str());
disconnectWithReason(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
app.DebugPrintf("[Auth-Client] JoinServer succeeded, sending AuthResponse(mojang)\n");
send(make_shared<AuthResponsePacket>(L"mojang", wUuid, wName));
}
else if (hasOnlineAccount && provider == "elyby" && serverHasElyby)
{
app.DebugPrintf("[Auth-Client] Choosing 'elyby' auth scheme\n");
string token = session.accessToken;
string uuid = MCAuth::UndashUuid(session.uuid);
string error;
bool ok = MCAuth::ElybyJoinServer(token, uuid, serverId, error);
if (!ok)
{
app.DebugPrintf("[Auth-Client] ElybyJoinServer FAILED: %s\n", error.c_str());
disconnectWithReason(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
app.DebugPrintf("[Auth-Client] ElybyJoinServer succeeded, sending AuthResponse(elyby)\n");
send(make_shared<AuthResponsePacket>(L"elyby", wUuid, wName));
}
else if (serverHasOffline)
{
app.DebugPrintf("[Auth-Client] Choosing 'offline' auth scheme\n");
send(make_shared<AuthResponsePacket>(L"offline", wUuid, wName));
}
else
{
app.DebugPrintf("[Auth-Client] No compatible auth scheme. Your account uses '%s' but the server requires%s%s%s. Switch account or ask the server admin to enable your provider.\n",
provider.c_str(),
serverHasMojang ? " mojang" : "",
serverHasElyby ? " elyby" : "",
serverHasOffline ? " offline" : "");
disconnectWithReason(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
#else
// Non-Windows64 platforms: MCAuth not available, try offline scheme only
bool serverHasOffline = false;
for (auto& s : packet->schemes)
{
if (s == L"offline") serverHasOffline = true;
}
if (serverHasOffline)
{
wstring wName = minecraft->user->name;
send(make_shared<AuthResponsePacket>(L"offline", L"", wName));
}
else
{
disconnectWithReason(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
#endif
}
void ClientConnection::handleAuthResult(shared_ptr<AuthResultPacket> packet)
{
app.DebugPrintf("[Auth-Client] Received AuthResultPacket: success=%d\n", (int)packet->success);
if (!packet->success)
{
string err(packet->errorMessage.begin(), packet->errorMessage.end());
app.DebugPrintf("[Auth-Client] Auth REJECTED by server: '%s'\n", err.c_str());
disconnectWithReason(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
m_assignedUuid = string(packet->assignedUuid.begin(), packet->assignedUuid.end());
m_assignedUsername = string(packet->assignedUsername.begin(), packet->assignedUsername.end());
app.DebugPrintf("[Auth-Client] Auth SUCCESS: assigned username='%s', uuid='%s'\n",
m_assignedUsername.c_str(), m_assignedUuid.c_str());
// Update the IQNet gamertag for THIS connection's slot so the TAB player list
// shows the auth-verified name. Each slot writes to its OWN m_player entry.
// user->name is only updated for the primary player (slot 0) since it's a
// shared global — splitscreen players must not clobber it.
if (!m_assignedUsername.empty())
{
wstring wName(m_assignedUsername.begin(), m_assignedUsername.end());
if (m_userIndex < MINECRAFT_NET_MAX_PLAYERS)
wcsncpy_s(IQNet::m_player[m_userIndex].m_gamertag, 32, wName.c_str(), _TRUNCATE);
if (m_userIndex == 0)
minecraft->user->name = wName;
}
#ifdef _WINDOWS64
// Store the Mojang skin received inline from the server (no separate download needed)
if (!packet->skinKey.empty() && !packet->skinData.empty() &&
MCAuth::ValidateSkinPng(packet->skinData.data(), packet->skinData.size()))
{
m_mojangSkinUrl = packet->skinKey;
if (m_userIndex < XUSER_MAX_COUNT)
app.m_mojangSkinKey[m_userIndex] = m_mojangSkinUrl;
DWORD skinSize = (DWORD)packet->skinData.size();
PBYTE skinBuf = new BYTE[skinSize];
memcpy(skinBuf, packet->skinData.data(), skinSize);
app.AddMemoryTextureFile(m_mojangSkinUrl, skinBuf, skinSize);
app.DebugPrintf("Client received Mojang skin inline: %zu bytes\n", packet->skinData.size());
}
else if (!packet->skinData.empty())
{
app.DebugPrintf("[Auth-Client] Rejected invalid skin data (%zu bytes)\n", packet->skinData.size());
}
#endif
// Send LoginPacket immediately — it serves as the auth handshake acknowledgement
sendLoginPacketAfterAuth();
}
void ClientConnection::sendLoginPacketAfterAuth()
{
// Priority: auth-assigned > MCAuth session > user->name (shared global, always player 1 in splitscreen)
wstring loginName;
if (!m_assignedUsername.empty())
{
loginName = wstring(m_assignedUsername.begin(), m_assignedUsername.end());
}
#ifdef _WINDOWS64
else
{
auto& mgr = MCAuthManager::Get();
if (mgr.IsSlotLoggedIn(m_userIndex))
{
auto session = mgr.GetSlotSession(m_userIndex);
if (!session.username.empty())
loginName = wstring(session.username.begin(), session.username.end());
}
}
#endif
if (loginName.empty())
{
loginName = minecraft->user->name;
}
PlayerUID loginUuid = INVALID_XUID;
if (!m_assignedUuid.empty())
{
loginUuid = GameUUID::fromDashed(m_assignedUuid);
}
if (!loginUuid.isValid())
{
loginUuid = GameUUID::generateOffline(std::string(loginName.begin(), loginName.end()));
}
BOOL allAllowed, friendsAllowed;
ProfileManager.AllowedPlayerCreatedContent(m_userIndex,true,&allAllowed,&friendsAllowed);
auto loginPacket = std::make_shared<LoginPacket>(loginName, SharedConstants::NETWORK_PROTOCOL_VERSION, loginUuid, (allAllowed != TRUE && friendsAllowed == TRUE),
m_cachedUgcPlayersVersion, app.GetPlayerSkinId(m_userIndex), app.GetPlayerCapeId(m_userIndex), ProfileManager.IsGuest(m_userIndex));
if (!m_assignedUuid.empty())
{
loginPacket->m_mojangUuid = wstring(m_assignedUuid.begin(), m_assignedUuid.end());
}
send(loginPacket);
if(!g_NetworkManager.IsHost() )
{
Minecraft::GetInstance()->progressRenderer->progressStagePercentage((eCCLoginSent * 100)/ (eCCConnected));
}
}
void ClientConnection::handleAddMob(shared_ptr<AddMobPacket> packet) void ClientConnection::handleAddMob(shared_ptr<AddMobPacket> packet)
{ {
double x = packet->x / 32.0; double x = packet->x / 32.0;
@ -2536,6 +2804,12 @@ void ClientConnection::handleAddMob(shared_ptr<AddMobPacket> packet)
shared_ptr<Slime> slime = dynamic_pointer_cast<Slime>(mob); shared_ptr<Slime> slime = dynamic_pointer_cast<Slime>(mob);
slime->setSize( slime->getSize() ); slime->setSize( slime->getSize() );
} }
// Retry any SetEntityLinkPackets that were waiting for this mob to be added.
// Without this, a deferred link where this mob is the source or dest would
// never be resolved (the original code only called this from handleAddEntity,
// missing mobs added via AddMobPacket).
checkDeferredEntityLinkPackets(packet->id);
} }
void ClientConnection::handleSetTime(shared_ptr<SetTimePacket> packet) void ClientConnection::handleSetTime(shared_ptr<SetTimePacket> packet)
@ -2557,13 +2831,31 @@ void ClientConnection::handleEntityLinkPacket(shared_ptr<SetEntityLinkPacket> pa
shared_ptr<Entity> sourceEntity = getEntity(packet->sourceId); shared_ptr<Entity> sourceEntity = getEntity(packet->sourceId);
shared_ptr<Entity> destEntity = getEntity(packet->destId); shared_ptr<Entity> destEntity = getEntity(packet->destId);
// 4J: If the destination entity couldn't be found, defer handling of this packet // 4J: If either entity couldn't be found, defer handling of this packet.
// This was added to support leashing (the entity link packet is sent before the add entity packet) //
if (destEntity == nullptr && packet->destId >= 0) // With real TCP splitscreen connections, the server's EntityTracker can send
// a SetEntityLinkPacket (e.g. villager riding a boat, player on a horse)
// BEFORE the AddEntityPacket/AddMobPacket/AddPlayerPacket for one of the
// linked entities has been received by this connection. This happens because:
// - TrackedEntity::tick() broadcasts riding state every 3 seconds to all
// connections currently tracking the entity.
// - TrackedEntity::updatePlayer() sends AddEntity then SetEntityLink, but
// processes entities in arbitrary order, so entity A's link to entity B
// can arrive before entity B's AddEntity.
// - A dead splitscreen player's connection may have stale entity tracking
// state, causing gaps when entity updates resume.
//
// Original 4J code only deferred when destEntity was missing (for leashing).
// Extended to also defer when sourceEntity is missing, since the same race
// occurs for the source (observed crash during splitscreen with a dead player
// and AFK player in a village — villager/mob entity link arrived before the
// source entity was added to this connection's world).
//
// The deferred packet is retried when the missing entity arrives
// (see checkDeferredEntityLinkPackets), or dropped after 1000ms timeout.
if ((destEntity == nullptr && packet->destId >= 0) ||
(sourceEntity == nullptr && packet->sourceId >= 0))
{ {
// We don't handle missing source entities because it shouldn't happen
assert(!(sourceEntity == nullptr && packet->sourceId >= 0));
deferredEntityLinkPackets.push_back(DeferredEntityLinkPacket(packet)); deferredEntityLinkPackets.push_back(DeferredEntityLinkPacket(packet));
return; return;
} }
@ -2728,6 +3020,16 @@ void ClientConnection::handleTextureAndGeometry(shared_ptr<TextureAndGeometryPac
#ifndef _CONTENT_PACKAGE #ifndef _CONTENT_PACKAGE
wprintf(L"Client received custom TextureAndGeometry %ls\n",packet->textureName.c_str()); wprintf(L"Client received custom TextureAndGeometry %ls\n",packet->textureName.c_str());
#endif #endif
// Validate mojang skin PNGs before loading into memory
bool isMojangSkin = (packet->textureName.length() > 6 &&
packet->textureName.substr(0, 6) == L"mojang");
if (isMojangSkin && !MCAuth::ValidateSkinPng(packet->pbData, packet->dwTextureBytes))
{
app.DebugPrintf("Rejected invalid mojang skin texture %ls (%u bytes)\n",
packet->textureName.c_str(), packet->dwTextureBytes);
return;
}
// Add the texture data // Add the texture data
app.AddMemoryTextureFile(packet->textureName,packet->pbData,packet->dwTextureBytes); app.AddMemoryTextureFile(packet->textureName,packet->pbData,packet->dwTextureBytes);
// Add the geometry data // Add the geometry data
@ -2824,6 +3126,12 @@ void ClientConnection::handleTextureAndGeometryChange(shared_ptr<TextureAndGeome
player->setCustomSkin( app.getSkinIdFromPath( packet->path ) ); player->setCustomSkin( app.getSkinIdFromPath( packet->path ) );
// Override customTextureUrl for Mojang skins (setCustomSkin resolves to a DLC path)
if (!packet->path.empty() && packet->path.substr(0, 3).compare(L"def") != 0)
{
player->customTextureUrl = packet->path;
}
#ifndef _CONTENT_PACKAGE #ifndef _CONTENT_PACKAGE
wprintf(L"Skin for remote player %ls has changed to %ls (%d)\n", player->name.c_str(), player->customTextureUrl.c_str(), player->getPlayerDefaultSkin() ); wprintf(L"Skin for remote player %ls has changed to %ls (%d)\n", player->name.c_str(), player->customTextureUrl.c_str(), player->getPlayerDefaultSkin() );
#endif #endif
@ -4090,31 +4398,48 @@ void ClientConnection::handleUpdateAttributes(shared_ptr<UpdateAttributesPacket>
} }
} }
// 4J: Check for deferred entity link packets related to this entity ID and handle them // Called when a new entity is added to the world (from handleAddEntity,
// handleAddMob, or handleAddPlayer). Checks whether any previously deferred
// SetEntityLinkPackets were waiting for this entity and retries them.
//
// Original 4J code only matched on destId (the destination/mount entity).
// Extended to also match sourceId so that packets deferred because the
// *source* entity was missing (e.g. a player not yet added during splitscreen
// join) are also retried when that entity arrives.
//
// Snapshot vector size before iterating -- handleEntityLinkPacket may re-defer (push_back).
// Without the snapshot limit we would process the newly pushed entry in the
// same pass, match it again on the same newEntityId, and loop forever.
// Newly deferred entries will be picked up on the next entity arrival.
void ClientConnection::checkDeferredEntityLinkPackets(int newEntityId) void ClientConnection::checkDeferredEntityLinkPackets(int newEntityId)
{ {
if (deferredEntityLinkPackets.empty()) return; if (deferredEntityLinkPackets.empty()) return;
for (size_t i = 0; i < deferredEntityLinkPackets.size(); i++) size_t originalCount = deferredEntityLinkPackets.size();
{
DeferredEntityLinkPacket *deferred = &deferredEntityLinkPackets[i];
for (size_t i = 0; i < originalCount; i++)
{
bool remove = false; bool remove = false;
// Only consider recently deferred packets // Only consider recently deferred packets (within 1000ms)
int tickInterval = GetTickCount() - deferred->m_recievedTick; int tickInterval = GetTickCount() - deferredEntityLinkPackets[i].m_recievedTick;
if (tickInterval < MAX_ENTITY_LINK_DEFERRAL_INTERVAL) if (tickInterval < MAX_ENTITY_LINK_DEFERRAL_INTERVAL)
{ {
// Note: we assume it's the destination entity // Match on both source and dest — either could be the newly arrived entity
if (deferred->m_packet->destId == newEntityId) shared_ptr<SetEntityLinkPacket> pkt = deferredEntityLinkPackets[i].m_packet;
if (pkt->destId == newEntityId || pkt->sourceId == newEntityId)
{ {
handleEntityLinkPacket(deferred->m_packet); // Retry the packet. If the OTHER entity is still missing,
// handleEntityLinkPacket will re-defer it (appended past
// originalCount, so we won't re-process it in this pass).
handleEntityLinkPacket(pkt);
remove = true; remove = true;
} }
} }
else else
{ {
// This is an old packet, remove (shouldn't really come up but seems prudent) // Timed out — drop the packet. The entity link will be corrected
// by the server's periodic 3-second riding/leash broadcast.
remove = true; remove = true;
} }
@ -4122,6 +4447,7 @@ void ClientConnection::checkDeferredEntityLinkPackets(int newEntityId)
{ {
deferredEntityLinkPackets.erase(deferredEntityLinkPackets.begin() + i); deferredEntityLinkPackets.erase(deferredEntityLinkPackets.begin() + i);
i--; i--;
originalCount--; // the range we're iterating over just shrunk
} }
} }
} }

View file

@ -101,6 +101,7 @@ public:
virtual void handleEntityActionAtPosition(shared_ptr<EntityActionAtPositionPacket> packet); virtual void handleEntityActionAtPosition(shared_ptr<EntityActionAtPositionPacket> packet);
virtual void handlePreLogin(shared_ptr<PreLoginPacket> packet); virtual void handlePreLogin(shared_ptr<PreLoginPacket> packet);
void close(); void close();
void disconnectWithReason(DisconnectPacket::eDisconnectReason reason);
virtual void handleAddMob(shared_ptr<AddMobPacket> packet); virtual void handleAddMob(shared_ptr<AddMobPacket> packet);
virtual void handleSetTime(shared_ptr<SetTimePacket> packet); virtual void handleSetTime(shared_ptr<SetTimePacket> packet);
virtual void handleSetSpawn(shared_ptr<SetSpawnPositionPacket> packet); virtual void handleSetSpawn(shared_ptr<SetSpawnPositionPacket> packet);
@ -155,6 +156,10 @@ public:
virtual void handleUpdateGameRuleProgressPacket(shared_ptr<UpdateGameRuleProgressPacket> packet); virtual void handleUpdateGameRuleProgressPacket(shared_ptr<UpdateGameRuleProgressPacket> packet);
virtual void handleXZ(shared_ptr<XZPacket> packet); virtual void handleXZ(shared_ptr<XZPacket> packet);
// Auth handshake
virtual void handleAuthScheme(shared_ptr<AuthSchemePacket> packet);
virtual void handleAuthResult(shared_ptr<AuthResultPacket> packet);
void displayPrivilegeChanges(shared_ptr<MultiplayerLocalPlayer> player, unsigned int oldPrivileges); void displayPrivilegeChanges(shared_ptr<MultiplayerLocalPlayer> player, unsigned int oldPrivileges);
virtual void handleAddObjective(shared_ptr<SetObjectivePacket> packet); virtual void handleAddObjective(shared_ptr<SetObjectivePacket> packet);
@ -179,4 +184,11 @@ private:
static const int MAX_ENTITY_LINK_DEFERRAL_INTERVAL = 1000; static const int MAX_ENTITY_LINK_DEFERRAL_INTERVAL = 1000;
void checkDeferredEntityLinkPackets(int newEntityId); void checkDeferredEntityLinkPackets(int newEntityId);
// Auth handshake state
std::string m_assignedUuid; // UUID assigned by server after auth
std::string m_assignedUsername; // username assigned by server
wstring m_mojangSkinUrl; // Mojang skin key from server (e.g. "mojang_skin_{uuid}.png")
DWORD m_cachedUgcPlayersVersion = 0; // stored from PreLoginPacket for later LoginPacket
void sendLoginPacketAfterAuth();
}; };

View file

@ -32,6 +32,9 @@
#include "..\Xbox\XML\xmlFilesCallback.h" #include "..\Xbox\XML\xmlFilesCallback.h"
#endif #endif
#include "Minecraft_Macros.h" #include "Minecraft_Macros.h"
#ifdef _WINDOWS64
#include "..\..\MCAuth\include\MCAuthManager.h"
#endif
#include "..\PlayerList.h" #include "..\PlayerList.h"
#include "..\ServerPlayer.h" #include "..\ServerPlayer.h"
#include "GameRules\ConsoleGameRules.h" #include "GameRules\ConsoleGameRules.h"
@ -153,8 +156,6 @@ CMinecraftApp::CMinecraftApp()
//ZeroMemory(m_PreviewBuffer,sizeof(XSOCIAL_PREVIEWIMAGE)*XUSER_MAX_COUNT); //ZeroMemory(m_PreviewBuffer,sizeof(XSOCIAL_PREVIEWIMAGE)*XUSER_MAX_COUNT);
m_xuidNotch = INVALID_XUID;
ZeroMemory(&m_InviteData,sizeof(JoinFromInviteData) ); ZeroMemory(&m_InviteData,sizeof(JoinFromInviteData) );
// m_bRead_TMS_XUIDS_XML=false; // m_bRead_TMS_XUIDS_XML=false;
@ -258,6 +259,7 @@ void CMinecraftApp::DebugPrintf(const char *szFormat, ...)
vsnprintf(buf, sizeof(buf), szFormat, ap); vsnprintf(buf, sizeof(buf), szFormat, ap);
va_end(ap); va_end(ap);
OutputDebugStringA(buf); OutputDebugStringA(buf);
fputs(buf, stdout);
#endif #endif
} }
@ -1640,6 +1642,18 @@ void CMinecraftApp::SetPlayerSkin(int iPad,DWORD dwSkinId)
} }
void CMinecraftApp::SetPlayerMojangSkin(int iPad)
{
SetPlayerSkin(iPad, (DWORD)eDefaultSkins_MojangSkin);
// Override customTextureUrl to point to this player's Mojang memory texture
if (iPad >= 0 && iPad < XUSER_MAX_COUNT &&
!m_mojangSkinKey[iPad].empty() && Minecraft::GetInstance()->localplayers[iPad] != nullptr)
{
Minecraft::GetInstance()->localplayers[iPad]->customTextureUrl = m_mojangSkinKey[iPad];
}
}
wstring CMinecraftApp::GetPlayerSkinName(int iPad) wstring CMinecraftApp::GetPlayerSkinName(int iPad)
{ {
return app.getSkinPathFromId(GameSettingsA[iPad]->dwSelectedSkin); return app.getSkinPathFromId(GameSettingsA[iPad]->dwSelectedSkin);
@ -3298,6 +3312,14 @@ void CMinecraftApp::HandleXuiActions(void)
// 4J Stu - Fix for #13257 - CRASH: Gameplay: Title crashed after exiting the tutorial // 4J Stu - Fix for #13257 - CRASH: Gameplay: Title crashed after exiting the tutorial
// It doesn't matter if they were in the tutorial already // It doesn't matter if they were in the tutorial already
pMinecraft->playerLeftTutorial( idx ); pMinecraft->playerLeftTutorial( idx );
#ifdef _WINDOWS64
// Clear splitscreen auth slots when leaving the world so the
// account picker doesn't show stale "Player N" badges in the
// main menu. Keep slot 0 (primary player) intact.
if (idx != (unsigned int)ProfileManager.GetPrimaryPad())
MCAuthManager::Get().ClearSlot(idx);
#endif
} }
LoadingInputParams *loadingParams = new LoadingInputParams(); LoadingInputParams *loadingParams = new LoadingInputParams();
@ -4560,6 +4582,8 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter )
int exitReasonStringId = -1; int exitReasonStringId = -1;
bool saveStats = false; bool saveStats = false;
app.DebugPrintf("[Auth-Client] ExitWorld: isClientSide=%d, IsInSession=%d, lpParameter=%p, disconnectReason=%d\n",
(int)pMinecraft->isClientSide(), (int)g_NetworkManager.IsInSession(), lpParameter, (int)app.GetDisconnectReason());
if (pMinecraft->isClientSide() || g_NetworkManager.IsInSession() ) if (pMinecraft->isClientSide() || g_NetworkManager.IsInSession() )
{ {
if(lpParameter != nullptr ) if(lpParameter != nullptr )
@ -4592,6 +4616,7 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter )
default: default:
exitReasonStringId = IDS_DISCONNECTED; exitReasonStringId = IDS_DISCONNECTED;
} }
app.DebugPrintf("[Auth-Client] ExitWorld: showing exitReasonStringId=%d\n", exitReasonStringId);
pMinecraft->progressRenderer->progressStartNoAbort( exitReasonStringId ); pMinecraft->progressRenderer->progressStartNoAbort( exitReasonStringId );
// 4J - Force a disconnection, this handles the situation that the server has already disconnected // 4J - Force a disconnection, this handles the situation that the server has already disconnected
if( pMinecraft->levels[0] != nullptr ) pMinecraft->levels[0]->disconnect(false); if( pMinecraft->levels[0] != nullptr ) pMinecraft->levels[0]->disconnect(false);
@ -4599,6 +4624,7 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter )
} }
else else
{ {
app.DebugPrintf("[Auth-Client] ExitWorld: lpParameter=null, showing IDS_EXITING_GAME\n");
exitReasonStringId = IDS_EXITING_GAME; exitReasonStringId = IDS_EXITING_GAME;
pMinecraft->progressRenderer->progressStartNoAbort( IDS_EXITING_GAME ); pMinecraft->progressRenderer->progressStartNoAbort( IDS_EXITING_GAME );
@ -4649,6 +4675,7 @@ int CMinecraftApp::SignoutExitWorldThreadProc( void* lpParameter )
break; break;
case DisconnectPacket::eDisconnect_OutdatedClient: case DisconnectPacket::eDisconnect_OutdatedClient:
exitReasonStringId = IDS_DISCONNECTED_CLIENT_OLD; exitReasonStringId = IDS_DISCONNECTED_CLIENT_OLD;
break;
default: default:
exitReasonStringId = IDS_DISCONNECTED; exitReasonStringId = IDS_DISCONNECTED;
} }
@ -5694,14 +5721,6 @@ void CMinecraftApp::UpdateTime()
m_Time.fAppTime = m_Time.fSecsPerTick * static_cast<FLOAT>(m_Time.qwAppTime.QuadPart); m_Time.fAppTime = m_Time.fSecsPerTick * static_cast<FLOAT>(m_Time.qwAppTime.QuadPart);
} }
bool CMinecraftApp::isXuidNotch(PlayerUID xuid)
{
if(m_xuidNotch != INVALID_XUID && xuid != INVALID_XUID)
{
return ProfileManager.AreXUIDSEqual(xuid, m_xuidNotch) == TRUE;
}
return false;
}
bool CMinecraftApp::isXuidDeadmau5(PlayerUID xuid) bool CMinecraftApp::isXuidDeadmau5(PlayerUID xuid)
{ {

View file

@ -349,7 +349,6 @@ public:
virtual void StoreLaunchData(); virtual void StoreLaunchData();
virtual void ExitGame(); virtual void ExitGame();
bool isXuidNotch(PlayerUID xuid);
bool isXuidDeadmau5(PlayerUID xuid); bool isXuidDeadmau5(PlayerUID xuid);
void AddMemoryTextureFile(const wstring &wName, PBYTE pbData, DWORD dwBytes); void AddMemoryTextureFile(const wstring &wName, PBYTE pbData, DWORD dwBytes);
@ -357,6 +356,10 @@ public:
void GetMemFileDetails(const wstring &wName,PBYTE *ppbData,DWORD *pdwBytes); void GetMemFileDetails(const wstring &wName,PBYTE *ppbData,DWORD *pdwBytes);
bool IsFileInMemoryTextures(const wstring &wName); bool IsFileInMemoryTextures(const wstring &wName);
// Mojang skin key per player slot (set after auth, e.g. "mojang_skin_{uuid}.png")
wstring m_mojangSkinKey[XUSER_MAX_COUNT];
void SetPlayerMojangSkin(int iPad);
// Texture Pack Data files (icon, banner, comparison shot & text) // Texture Pack Data files (icon, banner, comparison shot & text)
void AddMemoryTPDFile(int iConfig,PBYTE pbData,DWORD dwBytes); void AddMemoryTPDFile(int iConfig,PBYTE pbData,DWORD dwBytes);
void RemoveMemoryTPDFile(int iConfig); void RemoveMemoryTPDFile(int iConfig);
@ -378,7 +381,6 @@ public:
void AddCreditText(LPCWSTR lpStr); void AddCreditText(LPCWSTR lpStr);
private: private:
PlayerUID m_xuidNotch;
#ifdef _DURANGO #ifdef _DURANGO
unordered_map<PlayerUID, PBYTE, PlayerUID::Hash> m_GTS_Files; unordered_map<PlayerUID, PBYTE, PlayerUID::Hash> m_GTS_Files;
#else #else

Binary file not shown.

Binary file not shown.

View file

@ -43,7 +43,7 @@
#ifdef _WINDOWS64 #ifdef _WINDOWS64
#include "..\..\Windows64\Network\WinsockNetLayer.h" #include "..\..\Windows64\Network\WinsockNetLayer.h"
#include "..\..\Windows64\Windows64_Xuid.h" #include "..\..\Windows64\Windows64_Uuid.h"
#endif #endif
// Global instance // Global instance
@ -476,7 +476,7 @@ bool CGameNetworkManager::StartNetworkGame(Minecraft *minecraft, LPVOID lpParame
{ {
INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx); INetworkPlayer *pNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx);
Socket *socket = pNetworkPlayer->GetSocket(); Socket *socket = pNetworkPlayer->GetSocket();
app.DebugPrintf("Closing socket due to player %d not being signed in any more\n"); app.DebugPrintf("Closing socket due to player %d not being signed in any more\n", idx);
if( !socket->close(false) ) socket->close(true); if( !socket->close(false) ) socket->close(true);
continue; continue;
@ -698,7 +698,7 @@ bool CGameNetworkManager::IsPrivateGame()
return s_pPlatformNetworkManager->IsPrivateGame(); return s_pPlatformNetworkManager->IsPrivateGame();
} }
void CGameNetworkManager::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots, unsigned char privateSlots) void CGameNetworkManager::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots, int privateSlots)
{ {
// 4J Stu - clear any previous connection errors // 4J Stu - clear any previous connection errors
Minecraft::GetInstance()->clearConnectionFailed(); Minecraft::GetInstance()->clearConnectionFailed();
@ -1544,7 +1544,7 @@ void CGameNetworkManager::CreateSocket( INetworkPlayer *pNetworkPlayer, bool loc
// The NetworkPlayerXbox created by NotifyPlayerJoined already points to // The NetworkPlayerXbox created by NotifyPlayerJoined already points to
// m_player[padIdx], so we just set the smallId for network routing. // m_player[padIdx], so we just set the smallId for network routing.
IQNet::m_player[padIdx].m_smallId = assignedSmallId; IQNet::m_player[padIdx].m_smallId = assignedSmallId;
IQNet::m_player[padIdx].m_resolvedXuid = Win64Xuid::DeriveXuidForPad(Win64Xuid::ResolvePersistentXuid(), padIdx); IQNet::m_player[padIdx].m_resolvedXuid = INVALID_XUID; // Will be set by auth handshake
// Network socket (not hostLocal) — data goes through TCP via GetLocalSocket // Network socket (not hostLocal) — data goes through TCP via GetLocalSocket
socket = new Socket(pNetworkPlayer, false, false); socket = new Socket(pNetworkPlayer, false, false);

View file

@ -88,7 +88,7 @@ public:
bool IsLocalGame(); bool IsLocalGame();
void SetPrivateGame(bool isPrivate); void SetPrivateGame(bool isPrivate);
bool IsPrivateGame(); bool IsPrivateGame();
void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
bool IsHost(); bool IsHost();
bool IsInStatsEnabledSession(); bool IsInStatsEnabledSession();

View file

@ -68,7 +68,7 @@ public:
virtual void SendInviteGUI(int quadrant) = 0; virtual void SendInviteGUI(int quadrant) = 0;
virtual bool IsAddingPlayer() = 0; virtual bool IsAddingPlayer() = 0;
virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0) = 0; virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0) = 0;
virtual int JoinGame(FriendSessionInfo *searchResult, int dwLocalUsersMask, int dwPrimaryUserIndex ) = 0; virtual int JoinGame(FriendSessionInfo *searchResult, int dwLocalUsersMask, int dwPrimaryUserIndex ) = 0;
virtual void CancelJoinGame() {}; virtual void CancelJoinGame() {};
virtual bool SetLocalGame(bool isLocal) = 0; virtual bool SetLocalGame(bool isLocal) = 0;
@ -88,7 +88,7 @@ public:
private: private:
virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom) = 0; virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom) = 0;
virtual void _HostGame(int usersMask, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0) = 0; virtual void _HostGame(int usersMask, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0) = 0;
virtual bool _StartGame() = 0; virtual bool _StartGame() = 0;

View file

@ -5,7 +5,7 @@
#include "..\..\Xbox\Network\NetworkPlayerXbox.h" #include "..\..\Xbox\Network\NetworkPlayerXbox.h"
#ifdef _WINDOWS64 #ifdef _WINDOWS64
#include "..\..\Windows64\Network\WinsockNetLayer.h" #include "..\..\Windows64\Network\WinsockNetLayer.h"
#include "..\..\Windows64\Windows64_Xuid.h" #include "..\..\Windows64\Windows64_Uuid.h"
#include "..\..\Minecraft.h" #include "..\..\Minecraft.h"
#include "..\..\User.h" #include "..\..\User.h"
#include "..\..\MinecraftServer.h" #include "..\..\MinecraftServer.h"
@ -332,7 +332,7 @@ bool CPlatformNetworkManagerStub::AddLocalPlayerByUserIndex( int userIndex )
bool CPlatformNetworkManagerStub::RemoveLocalPlayerByUserIndex( int userIndex ) bool CPlatformNetworkManagerStub::RemoveLocalPlayerByUserIndex( int userIndex )
{ {
#ifdef _WINDOWS64 #ifdef _WINDOWS64
if (userIndex > 0 && userIndex < XUSER_MAX_COUNT && !m_pIQNet->IsHost()) if (userIndex > 0 && userIndex < XUSER_MAX_COUNT)
{ {
IQNetPlayer* qp = &IQNet::m_player[userIndex]; IQNetPlayer* qp = &IQNet::m_player[userIndex];
@ -343,7 +343,15 @@ bool CPlatformNetworkManagerStub::RemoveLocalPlayerByUserIndex( int userIndex )
} }
// Close the split-screen TCP connection and reset WinsockNetLayer state // Close the split-screen TCP connection and reset WinsockNetLayer state
WinsockNetLayer::CloseSplitScreenConnection(userIndex); if (!m_pIQNet->IsHost())
WinsockNetLayer::CloseSplitScreenConnection(userIndex);
// Release Mojang skin memory texture for this slot
if (!app.m_mojangSkinKey[userIndex].empty())
{
app.RemoveMemoryTextureFile(app.m_mojangSkinKey[userIndex]);
app.m_mojangSkinKey[userIndex].clear();
}
// Clear the IQNet slot so it can be reused on rejoin // Clear the IQNet slot so it can be reused on rejoin
qp->m_smallId = 0; qp->m_smallId = 0;
@ -418,7 +426,7 @@ bool CPlatformNetworkManagerStub::_LeaveGame(bool bMigrateHost, bool bLeaveRoom)
return true; return true;
} }
void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
// #ifdef _XBOX // #ifdef _XBOX
// 4J Stu - We probably did this earlier as well, but just to be sure! // 4J Stu - We probably did this earlier as well, but just to be sure!
@ -437,9 +445,8 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame,
#ifdef _WINDOWS64 #ifdef _WINDOWS64
IQNet::m_player[0].m_smallId = 0; IQNet::m_player[0].m_smallId = 0;
IQNet::m_player[0].m_isRemote = false; IQNet::m_player[0].m_isRemote = false;
// world host is pinned to legacy host XUID to keep old player data compatibility.
IQNet::m_player[0].m_isHostPlayer = true; IQNet::m_player[0].m_isHostPlayer = true;
IQNet::m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid(); IQNet::m_player[0].m_resolvedXuid = INVALID_XUID;
IQNet::s_playerCount = 1; IQNet::s_playerCount = 1;
#endif #endif
@ -482,7 +489,7 @@ void CPlatformNetworkManagerStub::HostGame(int localUsersMask, bool bOnlineGame,
//#endif //#endif
} }
void CPlatformNetworkManagerStub::_HostGame(int usersMask, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerStub::_HostGame(int usersMask, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
} }
@ -511,8 +518,7 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l
IQNet::m_player[0].m_smallId = 0; IQNet::m_player[0].m_smallId = 0;
IQNet::m_player[0].m_isRemote = true; IQNet::m_player[0].m_isRemote = true;
IQNet::m_player[0].m_isHostPlayer = true; IQNet::m_player[0].m_isHostPlayer = true;
// Remote host still maps to legacy host XUID in mixed old/new sessions. IQNet::m_player[0].m_resolvedXuid = INVALID_XUID; // Will be set when host identity is received
IQNet::m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid();
wcsncpy_s(IQNet::m_player[0].m_gamertag, 32, searchResult->data.hostName, _TRUNCATE); wcsncpy_s(IQNet::m_player[0].m_gamertag, 32, searchResult->data.hostName, _TRUNCATE);
WinsockNetLayer::StopDiscovery(); WinsockNetLayer::StopDiscovery();
@ -528,8 +534,7 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l
IQNet::m_player[localSmallId].m_smallId = localSmallId; IQNet::m_player[localSmallId].m_smallId = localSmallId;
IQNet::m_player[localSmallId].m_isRemote = false; IQNet::m_player[localSmallId].m_isRemote = false;
IQNet::m_player[localSmallId].m_isHostPlayer = false; IQNet::m_player[localSmallId].m_isHostPlayer = false;
// Local non-host identity is the persistent uid.dat XUID. IQNet::m_player[localSmallId].m_resolvedXuid = INVALID_XUID;
IQNet::m_player[localSmallId].m_resolvedXuid = Win64Xuid::ResolvePersistentXuid();
Minecraft* pMinecraft = Minecraft::GetInstance(); Minecraft* pMinecraft = Minecraft::GetInstance();
wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str()); wcscpy_s(IQNet::m_player[localSmallId].m_gamertag, 32, pMinecraft->user->name.c_str());
@ -789,7 +794,9 @@ wstring CPlatformNetworkManagerStub::GatherRTTStats()
for(unsigned int i = 0; i < GetPlayerCount(); ++i) for(unsigned int i = 0; i < GetPlayerCount(); ++i)
{ {
IQNetPlayer *pQNetPlayer = static_cast<NetworkPlayerXbox *>(GetPlayerByIndex(i))->GetQNetPlayer(); INetworkPlayer* np = GetPlayerByIndex(i);
if (np == nullptr) continue;
IQNetPlayer *pQNetPlayer = static_cast<NetworkPlayerXbox *>(np)->GetQNetPlayer();
if(!pQNetPlayer->IsLocal()) if(!pQNetPlayer->IsLocal())
{ {
@ -972,6 +979,8 @@ void CPlatformNetworkManagerStub::removeNetworkPlayer(IQNetPlayer *pQNetPlayer)
{ {
if( *it == pNetworkPlayer ) if( *it == pNetworkPlayer )
{ {
delete *it;
pQNetPlayer->SetCustomDataValue(0);
currentNetworkPlayers.erase(it); currentNetworkPlayers.erase(it);
return; return;
} }

View file

@ -40,7 +40,7 @@ public:
virtual void SendInviteGUI(int quadrant); virtual void SendInviteGUI(int quadrant);
virtual bool IsAddingPlayer(); virtual bool IsAddingPlayer();
virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex ); virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex );
virtual bool SetLocalGame(bool isLocal); virtual bool SetLocalGame(bool isLocal);
virtual bool IsLocalGame() { return m_bIsOfflineGame; } virtual bool IsLocalGame() { return m_bIsOfflineGame; }
@ -59,7 +59,7 @@ public:
private: private:
bool isSystemPrimaryPlayer(IQNetPlayer *pQNetPlayer); bool isSystemPrimaryPlayer(IQNetPlayer *pQNetPlayer);
virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom); virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom);
virtual void _HostGame(int dwUsersMask, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void _HostGame(int dwUsersMask, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual bool _StartGame(); virtual bool _StartGame();
IQNet * m_pIQNet; // pointer to QNet interface IQNet * m_pIQNet; // pointer to QNet interface

View file

@ -751,7 +751,7 @@ bool CPlatformNetworkManagerSony::_LeaveGame(bool bMigrateHost, bool bLeaveRoom)
return true; return true;
} }
void CPlatformNetworkManagerSony::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerSony::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
// #ifdef _XBOX // #ifdef _XBOX
// 4J Stu - We probably did this earlier as well, but just to be sure! // 4J Stu - We probably did this earlier as well, but just to be sure!
@ -766,7 +766,7 @@ void CPlatformNetworkManagerSony::HostGame(int localUsersMask, bool bOnlineGame,
//#endif //#endif
} }
void CPlatformNetworkManagerSony::_HostGame(int usersMask, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerSony::_HostGame(int usersMask, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
// Start hosting a new game // Start hosting a new game

View file

@ -48,7 +48,7 @@ public:
virtual void SendInviteGUI(int quadrant); virtual void SendInviteGUI(int quadrant);
virtual bool IsAddingPlayer(); virtual bool IsAddingPlayer();
virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex ); virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex );
virtual bool SetLocalGame(bool isLocal); virtual bool SetLocalGame(bool isLocal);
virtual bool IsLocalGame(); virtual bool IsLocalGame();
@ -74,7 +74,7 @@ public:
private: private:
bool isSystemPrimaryPlayer(SQRNetworkPlayer *pQNetPlayer); bool isSystemPrimaryPlayer(SQRNetworkPlayer *pQNetPlayer);
virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom); virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom);
virtual void _HostGame(int dwUsersMask, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void _HostGame(int dwUsersMask, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual bool _StartGame(); virtual bool _StartGame();
#ifdef __PSVITA__ #ifdef __PSVITA__

View file

@ -508,6 +508,10 @@ void IUIScene_PauseMenu::_ExitWorld(LPVOID lpParameter)
exitReasonTitleId = IDS_CONNECTION_FAILED; exitReasonTitleId = IDS_CONNECTION_FAILED;
break; break;
#endif #endif
case DisconnectPacket::eDisconnect_AuthFailed:
exitReasonStringId = IDS_DISCONNECTED;
exitReasonTitleId = IDS_CONNECTION_FAILED;
break;
default: default:
exitReasonStringId = IDS_CONNECTION_LOST_SERVER; exitReasonStringId = IDS_CONNECTION_LOST_SERVER;
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,346 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
// NativeUI — Direct D3D11 2D rendering for SWF-free UIScene subclasses.
//
// Renders rectangles, text, and widgets via raw D3D11 draw calls,
// completely independent of the Tesselator/RenderManager pipeline.
//
// Usage:
//
// void MyScene::render(S32 w, S32 h, C4JRender::eViewportType vp)
// {
// if (!m_hasTickedOnce) return;
// NativeUI::BeginFrame();
//
// NativeUI::DrawRect(0, 0, 1280, 720, 0xCC000000u);
// NativeUI::DrawShadowText(640, 300, L"Hello",
// 0xFFFFFFFFu, 22.0f, ALIGN_CENTER_X);
//
// NativeUI::EndFrame();
// }
//
// Coordinate space: 1280 x 720 virtual canvas (resolution-independent).
// Color format: 0xAARRGGBB (alpha 0xFF = opaque).
// Font size: Height in virtual pixels. Bitmap font is 8 units tall;
// system scales to match. Common sizes: 8, 12, 14, 16, 22.
namespace NativeUI
{
// ---- Alignment flags ------------------------------------------
enum : uint32_t
{
ALIGN_LEFT = 0x00u,
ALIGN_RIGHT = 0x01u,
ALIGN_CENTER_X = 0x02u,
ALIGN_CENTER_Y = 0x04u,
ALIGN_BOTTOM = 0x08u,
};
// ---- Lifecycle ------------------------------------------------
void BeginFrame();
void EndFrame();
void Shutdown();
// ---- Input helpers (Windows64) -----------------------------------
// Convert window mouse coords to 1280x720 virtual canvas.
// Returns false if the window handle is invalid or has zero size.
bool GetMouseVirtual(float& outX, float& outY);
// ---- Primitives -----------------------------------------------
// Filled axis-aligned rectangle.
void DrawRect(float x, float y, float w, float h, uint32_t color);
// Fullscreen rectangle that covers the entire backbuffer (ignores 16:9 viewport).
// Use for dim overlays, backgrounds, etc. that must reach into pillarbox/letterbox bands.
void DrawRectFullscreen(uint32_t color);
// Filled rectangle with rounded corners (radius in virtual pixels).
void DrawRoundedRect(float x, float y, float w, float h,
float radius, uint32_t color);
// Rounded border (outline only, no fill).
void DrawRoundedBorder(float x, float y, float w, float h,
float radius, float thickness, uint32_t color);
// Outlined rectangle (border only, no fill).
void DrawBorder(float x, float y, float w, float h,
float thickness, uint32_t color);
// Horizontal line.
void DrawLine(float x, float y, float length, float thickness,
uint32_t color);
// Vertical line.
void DrawLineV(float x, float y, float length, float thickness,
uint32_t color);
// Filled rectangle with gradient (top to bottom).
void DrawGradientRect(float x, float y, float w, float h,
uint32_t topColor, uint32_t bottomColor);
// Rounded rect with gradient (top to bottom).
void DrawGradientRoundedRect(float x, float y, float w, float h,
float radius,
uint32_t topColor, uint32_t bottomColor);
// Drop shadow behind a rectangle (semi-transparent, offset).
void DrawDropShadow(float x, float y, float w, float h,
float offset = 4.0f, float spread = 6.0f,
uint32_t color = 0x60000000u);
// Panel with rounded corners, drop shadow, and optional border.
void DrawPanel(float x, float y, float w, float h,
float radius = 8.0f,
uint32_t bgColor = 0xF0181818u,
uint32_t borderColor = 0xFF333333u,
float borderThick = 1.0f,
bool shadow = true);
// Horizontal divider line with optional fade at edges.
void DrawDivider(float x, float y, float w,
uint32_t color = 0xFF333333u,
float thickness = 1.0f);
// ---- Text -----------------------------------------------------
void DrawText(float x, float y, const wchar_t* text,
uint32_t color, float size = 14.0f,
uint32_t align = ALIGN_LEFT);
void DrawShadowText(float x, float y, const wchar_t* text,
uint32_t color, float size = 14.0f,
uint32_t align = ALIGN_LEFT);
float DrawTextWrapped(float x, float y, const wchar_t* text,
float maxWidth, uint32_t color,
float size = 14.0f, uint32_t align = ALIGN_LEFT);
void MeasureText(const wchar_t* text, float size,
float* outWidth, float* outHeight);
float LineHeight(float size);
// ---- Clipping -------------------------------------------------
void PushClipRect(float x, float y, float w, float h);
void PopClipRect();
// ---- Widgets --------------------------------------------------
void DrawButton(float x, float y, float w, float h,
const wchar_t* label, bool focused,
bool hovered = false, float labelSize = 16.0f);
// Text input box from gui/gui.png (y=46, 20px tall in atlas).
void DrawTextBox(float x, float y, float w, float h,
uint32_t tint = 0xFFFFFFFFu);
// Clickable link text with underline. Opens URL on activation.
void DrawLink(float x, float y, const wchar_t* text,
const char* url, bool focused, bool hovered = false,
float size = 14.0f, uint32_t align = ALIGN_LEFT,
float* outX = nullptr, float* outY = nullptr,
float* outW = nullptr, float* outH = nullptr);
void DrawProgressBar(float x, float y, float w, float h,
float progress,
uint32_t fillColor = 0xFF1A71D1u,
uint32_t trackColor = 0xFF333333u);
void DrawSpinner(float cx, float cy, float radius, int tick,
uint32_t color = 0xFFFFFFFFu);
void DrawCheckbox(float x, float y, float size,
bool checked, bool focused,
bool hovered = false);
void DrawSlider(float x, float y, float w, float h,
float value, bool focused,
bool hovered = false,
uint32_t fillColor = 0xFF1A71D1u,
uint32_t trackColor = 0xFF333333u);
void DrawTooltip(float x, float y, const wchar_t* text,
float size = 12.0f);
// Open a URL in the system default browser (Windows only, no-op elsewhere).
void OpenURL(const char* url);
// ---- Texture drawing ------------------------------------------
// Load a texture from the game's resource pack (e.g. L"/gui/container.png").
// Returns an internal texture ID for use with Draw calls.
// Textures are cached — calling this multiple times with the same path is cheap.
int LoadTexture(const wchar_t* path);
// Load a pre-registered texture by TEXTURE_NAME enum value.
// See Textures.h for the full list (TN_GUI_GUI, TN_TERRAIN, etc.).
int LoadTextureByName(int textureName);
// Load a PNG file and create a D3D11 texture directly (bypasses RenderManager).
// Decodes via WIC, creates DXGI_FORMAT_R8G8B8A8_UNORM texture.
// Use this for externally sourced PNGs (e.g. Mojang skins) where the
// RenderManager's pixel format assumptions may not match.
// Returns texture ID (for use with DrawTexture/DrawTextureUV), or -1 on failure.
// Caches by path. Must be called from the main/render thread.
int LoadTextureFromFileDirect(const char* filePath,
int* outWidth = nullptr, int* outHeight = nullptr);
// Load a texture from an arbitrary filesystem path relative to the EXE dir.
// e.g. "Common/Media/Graphics/PanelsAndTabs/Panel_MM.png"
// Returns texture ID, or -1 on failure. Caches by path.
// Also returns texture dimensions via optional out params.
int LoadTextureFromFile(const char* filePath,
int* outWidth = nullptr, int* outHeight = nullptr);
// 9-slice panel: draws a scalable panel from 9 texture pieces.
// basePath = folder + prefix, e.g. "Common/Media/Graphics/PanelsAndTabs/Panel"
// Auto-detects naming convention:
// Convention A: _TL, _TM, _TR, _ML, _MM, _MR, _BL, _BM, _BR
// Convention B: _Top_L, _Top_M, _Top_R, _Mid_L, _Mid_M, _Mid_R, _Bot_L, _Bot_M, _Bot_R
struct NineSlice
{
int tl, tm, tr; // texture IDs: top-left, top-mid, top-right
int ml, mm, mr; // mid-left, mid-mid, mid-right
int bl, bm, br; // bot-left, bot-mid, bot-right
int cornerW, cornerH; // corner piece dimensions
bool valid;
};
// Load a 9-slice panel set from disk. Caches internally.
// Supports both naming conventions (auto-detected).
NineSlice LoadNineSlice(const char* basePath);
// Draw a 9-slice panel scaled to any size.
void DrawNineSlice(float x, float y, float w, float h,
const NineSlice& ns, uint32_t tint = 0xFFFFFFFFu);
// 3-slice horizontal strip (tabs, bars).
// basePath e.g. "Common/Media/Graphics/PanelsAndTabs/Tab"
// Expects: {basePath}_Left.png, _Middle.png, _Right.png
struct ThreeSlice
{
int left, mid, right; // texture IDs
int capW, capH; // left/right cap dimensions
bool valid;
};
// Load a 3-slice strip from disk. Caches internally.
ThreeSlice LoadThreeSlice(const char* basePath);
// Draw a 3-slice strip scaled horizontally.
void DrawThreeSlice(float x, float y, float w, float h,
const ThreeSlice& ts, uint32_t tint = 0xFFFFFFFFu);
// Draw a texture filling the given rect. tint multiplies the texture color.
// Pass 0xFFFFFFFF for no tint. textureId comes from LoadTexture/LoadTextureByName.
void DrawTexture(float x, float y, float w, float h,
int textureId, uint32_t tint = 0xFFFFFFFFu);
// Draw a sub-region of a texture (UV rect in [0,1] space).
void DrawTextureUV(float x, float y, float w, float h,
int textureId,
float u0, float v0, float u1, float v1,
uint32_t tint = 0xFFFFFFFFu);
// Draw a texture clipped to rounded corners (uses scissor).
void DrawTextureRounded(float x, float y, float w, float h,
int textureId, float radius,
uint32_t tint = 0xFFFFFFFFu);
// Draw a texture scaled to fit (maintaining aspect ratio, centered).
// texW/texH = source texture dimensions in pixels.
void DrawTextureFit(float x, float y, float w, float h,
int textureId, int texW, int texH,
uint32_t tint = 0xFFFFFFFFu);
// ---- Focus list -----------------------------------------------
class FocusList
{
public:
struct Entry { int id; float x, y, w, h; };
void Clear() { m_entries.clear(); m_hoveredId = -1; }
void Add(int id, float x, float y, float w, float h);
int GetFocused() const;
int GetHovered() const { return m_hoveredId; }
bool IsFocused(int id) const { return GetFocused() == id; }
bool IsHovered(int id) const { return m_lastDevice == eDevice_Mouse && m_hoveredId == id; }
bool IsActive(int id) const { return IsFocused(id) || IsHovered(id); }
// For rendering: show highlight based on last-used input device.
// Gamepad last → show focus ring on focused element, ignore mouse hover.
// Mouse last → show hover highlight, suppress gamepad focus ring.
bool ShowFocus(int id) const
{
if (m_lastDevice == eDevice_Mouse)
return m_hoveredId >= 0 && m_hoveredId == id;
// Gamepad or no device: show gamepad focus, ignore hover
return IsFocused(id);
}
void MoveNext();
void MovePrev();
void SetFocus(int id);
bool HitTest(float mx, float my, int& outId) const;
bool UpdateHover(float mx, float my);
void ClearHover() { m_hoveredId = -1; }
int Count() const { return (int)m_entries.size(); }
// Mouse hover — call once per tick from tick().
// Updates hover state, plays hover sound. No click handling.
void TickMouse();
// True while the mouse was used for the last interaction.
bool IsMouseConsumed() const { return m_mouseConsumed; }
// Gamepad/keyboard — standard menu key handling.
// Handles UP/DOWN/LEFT/RIGHT (navigation with sound),
// OK (returns focused id with press sound),
// CANCEL (returns backId with back sound).
//
// backId = the control id to return when CANCEL is pressed.
//
// Returns: >=0 = activated element id (OK or CANCEL)
// -1 = navigation happened (focus moved, no activation)
// -2 = key not handled (scene should set handled=false)
static constexpr int RESULT_NAVIGATED = -1;
static constexpr int RESULT_UNHANDLED = -2;
// panelX/Y/W/H = scene panel bounds. Used to hit-test mouse clicks
// when UIController sends ACTION_MENU_OK from a left-click.
int HandleMenuKey(int key, int backId = 0,
float panelX = 0, float panelY = 0,
float panelW = 0, float panelH = 0);
// Input device tracking — same concept as Iggy's lastUsedDevice.
// When the gamepad navigates, mouse hover is suppressed.
// When the mouse moves, gamepad focus ring is suppressed.
enum ELastDevice { eDevice_None, eDevice_Gamepad, eDevice_Mouse };
ELastDevice GetLastDevice() const { return m_lastDevice; }
private:
std::vector<Entry> m_entries;
int m_focusIdx = 0;
int m_hoveredId = -1;
int m_lastHoveredId = -1; // hover id from previous tick
bool m_mouseConsumed = false; // true = click in progress, don't re-fire
ELastDevice m_lastDevice = eDevice_None;
float m_lastMouseX = -1.0f;
float m_lastMouseY = -1.0f;
};
} // namespace NativeUI

View file

@ -88,6 +88,9 @@
#include "UIScene_SkinSelectMenu.h" #include "UIScene_SkinSelectMenu.h"
#include "UIScene_HowToPlayMenu.h" #include "UIScene_HowToPlayMenu.h"
#include "UIScene_LanguageSelector.h" #include "UIScene_LanguageSelector.h"
#ifndef MINECRAFT_SERVER_BUILD
#include "UIScene_MSAuth.h"
#endif
#include "UIScene_HowToPlay.h" #include "UIScene_HowToPlay.h"
#include "UIScene_ControlsMenu.h" #include "UIScene_ControlsMenu.h"
#include "UIScene_Credits.h" #include "UIScene_Credits.h"

View file

@ -127,6 +127,27 @@ this, Iggy allows us to install a callback that will be called
any time ActionScript code calls trace. */ any time ActionScript code calls trace. */
static void RADLINK TraceCallback(void *user_callback_data, Iggy *player, char const *utf8_string, S32 length_in_bytes) static void RADLINK TraceCallback(void *user_callback_data, Iggy *player, char const *utf8_string, S32 length_in_bytes)
{ {
if (length_in_bytes <= 0 || utf8_string == nullptr) return;
// Suppress per-frame HUD repositioning trace from ActionScript.
// The SWF emits "iSceneWidth = ..." every frame — skip any line containing it.
for (S32 i = 0; i <= length_in_bytes - 4; ++i) {
if (utf8_string[i] == 'i' && utf8_string[i+1] == 'S' &&
utf8_string[i+2] == 'c' && utf8_string[i+3] == 'e')
return;
}
// Also skip whitespace-only traces (newlines, spaces)
bool hasContent = false;
for (S32 i = 0; i < length_in_bytes; ++i) {
char c = utf8_string[i];
if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\0') {
hasContent = true;
break;
}
}
if (!hasContent) return;
app.DebugPrintf(app.USER_UI, (char *)utf8_string); app.DebugPrintf(app.USER_UI, (char *)utf8_string);
} }

View file

@ -128,6 +128,7 @@ enum EUIScene
eUIScene_EULA, eUIScene_EULA,
eUIScene_InGameSaveManagementMenu, eUIScene_InGameSaveManagementMenu,
eUIScene_LanguageSelector, eUIScene_LanguageSelector,
eUIScene_MSAuth, // Microsoft Account sign-in (device code flow)
#endif // ndef _XBOX #endif // ndef _XBOX
#ifdef _DEBUG_MENUS_ENABLED #ifdef _DEBUG_MENUS_ENABLED

View file

@ -293,6 +293,11 @@ bool UILayer::NavigateToScene(int iPad, EUIScene scene, void *initData)
case eUIScene_LanguageSelector: case eUIScene_LanguageSelector:
newScene = new UIScene_LanguageSelector(iPad, initData, this); newScene = new UIScene_LanguageSelector(iPad, initData, this);
break; break;
#ifndef MINECRAFT_SERVER_BUILD
case eUIScene_MSAuth:
newScene = new UIScene_MSAuth(iPad, initData, this);
break;
#endif
case eUIScene_HowToPlay: case eUIScene_HowToPlay:
newScene = new UIScene_HowToPlay(iPad, initData, this); newScene = new UIScene_HowToPlay(iPad, initData, this);
break; break;

View file

@ -97,13 +97,16 @@ void UIScene::reloadMovie(bool force)
updateComponents(); updateComponents();
handleReload(); handleReload();
IggyDataValue result; if (getMovie())
IggyDataValue value[1]; {
IggyDataValue result;
IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_number; value[0].type = IGGY_DATATYPE_number;
value[0].number = m_iFocusControl; value[0].number = m_iFocusControl;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetFocus , 1 , value ); IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetFocus , 1 , value );
}
m_needsCacheRendered = true; m_needsCacheRendered = true;
m_bIsReloading = false; m_bIsReloading = false;
@ -218,6 +221,8 @@ void UIScene::updateSafeZone()
void UIScene::setSafeZone(S32 safeTop, S32 safeBottom, S32 safeLeft, S32 safeRight) void UIScene::setSafeZone(S32 safeTop, S32 safeBottom, S32 safeLeft, S32 safeRight)
{ {
if (!getMovie()) return;
IggyDataValue result; IggyDataValue result;
IggyDataValue value[4]; IggyDataValue value[4];
@ -245,6 +250,8 @@ void UIScene::initialiseMovie()
#if defined(__PSVITA__) || defined(_WINDOWS64) #if defined(__PSVITA__) || defined(_WINDOWS64)
void UIScene::SetFocusToElement(int iID) void UIScene::SetFocusToElement(int iID)
{ {
if (!getMovie()) return;
IggyDataValue result; IggyDataValue result;
IggyDataValue value[1]; IggyDataValue value[1];
@ -260,6 +267,9 @@ void UIScene::SetFocusToElement(int iID)
bool UIScene::mapElementsAndNames() bool UIScene::mapElementsAndNames()
{ {
// Native scenes have no SWF; nothing to map.
if (!swf) return true;
m_rootPath = IggyPlayerRootPath( swf ); m_rootPath = IggyPlayerRootPath( swf );
m_funcRemoveObject = registerFastName( L"RemoveObject" ); m_funcRemoveObject = registerFastName( L"RemoveObject" );
@ -278,6 +288,19 @@ void UIScene::loadMovie()
EnterCriticalSection(&UIController::ms_reloadSkinCS); // MGH - added to prevent crash loading Iggy movies while the skins were being reloaded EnterCriticalSection(&UIController::ms_reloadSkinCS); // MGH - added to prevent crash loading Iggy movies while the skins were being reloaded
wstring moviePath = getMoviePath(); wstring moviePath = getMoviePath();
// Native (no-SWF) scenes return an empty path. Set up dimensions from
// the current screen size so render() has valid extents, mark as ticked
// so input handling is enabled, then bail out without touching Iggy.
if (moviePath.empty())
{
m_renderWidth = m_movieWidth = (S32)ui.getScreenWidth();
m_renderHeight = m_movieHeight = (S32)ui.getScreenHeight();
m_loadedResolution = (ui.getScreenHeight() > 720.0f) ? eSceneResolution_1080 : eSceneResolution_720;
m_hasTickedOnce = true;
LeaveCriticalSection(&UIController::ms_reloadSkinCS);
return;
}
#ifdef __PS3__ #ifdef __PS3__
if(RenderManager.IsWidescreen()) if(RenderManager.IsWidescreen())
{ {
@ -461,7 +484,12 @@ void UIScene::tick()
{ {
if(m_bIsReloading) return; if(m_bIsReloading) return;
if(m_hasTickedOnce) m_bCanHandleInput = true; if(m_hasTickedOnce) m_bCanHandleInput = true;
while(IggyPlayerReadyToTick( swf )) if (!swf)
{
// Native (no-SWF) scene: run timers without touching Iggy.
tickTimers();
}
else while(IggyPlayerReadyToTick( swf ))
{ {
tickTimers(); tickTimers();
for(auto & it : m_controls) for(auto & it : m_controls)
@ -655,6 +683,7 @@ IggyName UIScene::registerFastName(const wstring &name)
} }
else else
{ {
if (!getMovie()) return 0;
var = IggyPlayerCreateFastName ( getMovie() , (IggyUTF16 *)name.c_str() , -1 ); var = IggyPlayerCreateFastName ( getMovie() , (IggyUTF16 *)name.c_str() , -1 );
m_fastNames[name] = var; m_fastNames[name] = var;
} }
@ -663,6 +692,8 @@ IggyName UIScene::registerFastName(const wstring &name)
void UIScene::removeControl( UIControl_Base *control, bool centreScene) void UIScene::removeControl( UIControl_Base *control, bool centreScene)
{ {
if (!getMovie()) return;
IggyDataValue result; IggyDataValue result;
IggyDataValue value[2]; IggyDataValue value[2];
@ -692,18 +723,21 @@ void UIScene::removeControl( UIControl_Base *control, bool centreScene)
void UIScene::slideLeft() void UIScene::slideLeft()
{ {
if (!getMovie()) return;
IggyDataValue result; IggyDataValue result;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSlideLeft , 0 , nullptr ); IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSlideLeft , 0 , nullptr );
} }
void UIScene::slideRight() void UIScene::slideRight()
{ {
if (!getMovie()) return;
IggyDataValue result; IggyDataValue result;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSlideRight , 0 , nullptr ); IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSlideRight , 0 , nullptr );
} }
void UIScene::doHorizontalResizeCheck() void UIScene::doHorizontalResizeCheck()
{ {
if (!getMovie()) return;
IggyDataValue result; IggyDataValue result;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcHorizontalResizeCheck , 0 , nullptr ); IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcHorizontalResizeCheck , 0 , nullptr );
} }
@ -741,12 +775,15 @@ void UIScene::setOpacity(float percent)
if(m_bUpdateOpacity) if(m_bUpdateOpacity)
m_bUpdateOpacity = false; m_bUpdateOpacity = false;
IggyDataValue result; if (getMovie())
IggyDataValue value[1]; {
value[0].type = IGGY_DATATYPE_number; IggyDataValue result;
value[0].number = percent; IggyDataValue value[1];
value[0].type = IGGY_DATATYPE_number;
value[0].number = percent;
IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetAlpha , 1 , value ); IggyResult out = IggyPlayerCallMethodRS ( getMovie() , &result, IggyPlayerRootPath( getMovie() ), m_funcSetAlpha , 1 , value );
}
} }
} }
@ -1000,7 +1037,7 @@ void UIScene::gainFocus()
updateTooltips(); updateTooltips();
updateComponents(); updateComponents();
if(!m_bFocussedOnce) if(!m_bFocussedOnce && getMovie())
{ {
IggyDataValue result; IggyDataValue result;
IggyDataValue value[1]; IggyDataValue value[1];

View file

@ -3,6 +3,9 @@
#include "UIScene_CreateWorldMenu.h" #include "UIScene_CreateWorldMenu.h"
#include "..\..\MinecraftServer.h" #include "..\..\MinecraftServer.h"
#include "..\..\Minecraft.h" #include "..\..\Minecraft.h"
#ifdef _WINDOWS64
#include "..\..\..\MCAuth\include\MCAuthManager.h"
#endif
#include "..\..\Options.h" #include "..\..\Options.h"
#include "..\..\TexturePackRepository.h" #include "..\..\TexturePackRepository.h"
#include "..\..\TexturePack.h" #include "..\..\TexturePack.h"
@ -520,6 +523,21 @@ void UIScene_CreateWorldMenu::checkPrivilegeCallback(LPVOID lpParam, bool hasPri
void UIScene_CreateWorldMenu::StartSharedLaunchFlow() void UIScene_CreateWorldMenu::StartSharedLaunchFlow()
{ {
#ifdef _WINDOWS64
// Block world creation if no account is configured (empty username/uuid).
{
auto session = MCAuthManager::Get().GetSlotSession(0);
if (session.username.empty() || session.uuid.empty())
{
app.DebugPrintf("[Auth] No account configured, returning to main menu for auth\n");
m_bIgnoreInput = false;
ui.NavigateToHomeMenu();
ui.NavigateToScene(m_iPad, eUIScene_MSAuth);
return;
}
}
#endif
Minecraft *pMinecraft=Minecraft::GetInstance(); Minecraft *pMinecraft=Minecraft::GetInstance();
// Check if we need to upsell the texture pack // Check if we need to upsell the texture pack
if(m_MoreOptionsParams.dwTexturePack!=0) if(m_MoreOptionsParams.dwTexturePack!=0)

View file

@ -1,9 +1,28 @@
#define _CRT_SECURE_NO_WARNINGS
#include "stdafx.h" #include "stdafx.h"
#include "UI.h" #include "UI.h"
#if defined(__PS3__) || defined(__ORBIS__) #if defined(__PS3__) || defined(__ORBIS__)
#include "Common\Network\Sony\SonyCommerce.h" #include "Common\Network\Sony\SonyCommerce.h"
#endif #endif
#include "UIScene_DLCMainMenu.h" #include "UIScene_DLCMainMenu.h"
#include "../../../MCAuth/include/MCAuthManager.h"
#include <cstdio>
#include <cstdarg>
#if !defined(_FINAL_BUILD) && defined(_DEBUG)
static void DLCMenuLog(const char* fmt, ...) {
char buf[512];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
#ifdef _WIN32
OutputDebugStringA(buf);
#endif
}
#else
static void DLCMenuLog(const char* /*fmt*/, ...) {}
#endif
#define PLAYER_ONLINE_TIMER_ID 0 #define PLAYER_ONLINE_TIMER_ID 0
#define PLAYER_ONLINE_TIMER_TIME 100 #define PLAYER_ONLINE_TIMER_TIME 100
@ -30,13 +49,38 @@ UIScene_DLCMainMenu::UIScene_DLCMainMenu(int iPad, void *initData, UILayer *pare
m_bCategoriesShown=false; m_bCategoriesShown=false;
#endif #endif
if(m_loadedResolution == eSceneResolution_1080) // Try to restore a previously saved Java auth session (background refresh).
MCAuthManager::Get().TryRestoreActiveJavaAccount();
DLCMenuLog("[DLCMainMenu] After TryRestore: IsJavaLoggedIn=%d, State=%d\n",
(int)MCAuthManager::Get().IsJavaLoggedIn(),
(int)MCAuthManager::Get().GetState());
// Pre-warm: proactively refresh token if it's expiring soon
if (MCAuthManager::Get().IsJavaLoggedIn() && MCAuthManager::Get().IsTokenExpiringSoon(0))
{
MCAuthManager::Get().RefreshSlot(0);
}
{ {
#ifdef _DURANGO #ifdef _DURANGO
m_labelXboxStore.init(IDS_XBOX_STORE); m_labelXboxStore.init(IDS_XBOX_STORE);
#else #else
m_labelXboxStore.init( L"" ); m_labelXboxStore.init(L"");
#endif #endif
// Show Minecraft username if already signed in, otherwise show generic label
if(MCAuthManager::Get().IsJavaLoggedIn())
{
MCAuth::JavaSession session = MCAuthManager::Get().GetJavaSession();
std::wstring name(session.username.begin(), session.username.end());
DLCMenuLog("[DLCMainMenu] Constructor: Already logged in as '%s'\n",
session.username.c_str());
m_labelXboxStore.init(name);
m_authLabelUpdated = true;
}
else
{
m_labelXboxStore.init(L"Sign in with Microsoft");
}
} }
#if defined(_DURANGO) #if defined(_DURANGO)
@ -140,6 +184,12 @@ void UIScene_DLCMainMenu::handlePress(F64 controlId, F64 childId)
ui.NavigateToScene(m_iPad, eUIScene_DLCOffersMenu, param); ui.NavigateToScene(m_iPad, eUIScene_DLCOffersMenu, param);
break; break;
} }
case eControl_MSSignIn:
{
// Open the Microsoft Account sign-in dialog
ui.NavigateToScene(m_iPad, eUIScene_MSAuth, nullptr, eUILayer_Popup, eUIGroup_Fullscreen);
break;
}
}; };
} }
@ -182,8 +232,25 @@ void UIScene_DLCMainMenu::handleGainFocus(bool navBack)
updateTooltips(); updateTooltips();
// Allow tick() to re-check auth state after navigating back.
m_authLabelUpdated = false;
if(navBack) if(navBack)
{ {
// Refresh the sign-in button label after returning from MSAuth.
if(MCAuthManager::Get().IsJavaLoggedIn())
{
MCAuth::JavaSession s = MCAuthManager::Get().GetJavaSession();
std::wstring name(s.username.begin(), s.username.end());
DLCMenuLog("[DLCMainMenu] handleGainFocus: logged in as '%s'\n",
s.username.c_str());
m_labelXboxStore.setLabel(name);
}
else
{
m_labelXboxStore.setLabel(L"Sign in with Microsoft");
}
// add the timer back in // add the timer back in
#if ( defined __PS3__ || defined __ORBIS__ || defined __PSVITA__ ) #if ( defined __PS3__ || defined __ORBIS__ || defined __PSVITA__ )
addTimer( PLAYER_ONLINE_TIMER_ID, PLAYER_ONLINE_TIMER_TIME ); addTimer( PLAYER_ONLINE_TIMER_ID, PLAYER_ONLINE_TIMER_TIME );
@ -195,6 +262,32 @@ void UIScene_DLCMainMenu::tick()
{ {
UIScene::tick(); UIScene::tick();
// Update sign-in label when background session restore completes.
if(!m_authLabelUpdated)
{
auto state = MCAuthManager::Get().GetState();
bool loggedIn = MCAuthManager::Get().IsJavaLoggedIn();
if(loggedIn)
{
MCAuth::JavaSession s = MCAuthManager::Get().GetJavaSession();
std::wstring name(s.username.begin(), s.username.end());
DLCMenuLog("[DLCMainMenu] tick: Auth restored! username='%s', setting label\n",
s.username.c_str());
m_labelXboxStore.setLabel(name);
m_authLabelUpdated = true;
}
else if(state == MCAuthManager::State::Idle ||
state == MCAuthManager::State::Failed)
{
DLCMenuLog("[DLCMainMenu] tick: Restore finished without login (state=%d), stop polling\n",
(int)state);
// Restore attempt finished but failed — stop checking.
m_authLabelUpdated = true;
}
// else: still authenticating in background, keep polling
}
#if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) #if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__)
if((m_bCategoriesShown==false) && (app.GetCommerceCategoriesRetrieved())) if((m_bCategoriesShown==false) && (app.GetCommerceCategoriesRetrieved()))
{ {

View file

@ -8,23 +8,25 @@ private:
enum EControls enum EControls
{ {
eControl_OffersList, eControl_OffersList,
eControl_MSSignIn, // Microsoft Account sign-in button
}; };
UIControl_DynamicButtonList m_buttonListOffers; UIControl_DynamicButtonList m_buttonListOffers;
UIControl_Label m_labelOffers, m_labelXboxStore; UIControl_Label m_labelOffers, m_labelXboxStore;
UIControl m_buttonMSSignIn; // replaces Xbox Store label on non-Durango
UIControl m_Timer; UIControl m_Timer;
UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene)
UI_MAP_ELEMENT( m_buttonListOffers, "OffersList") UI_MAP_ELEMENT( m_buttonListOffers, "OffersList")
UI_MAP_ELEMENT( m_labelOffers, "OffersList_Title") UI_MAP_ELEMENT( m_labelOffers, "OffersList_Title")
UI_MAP_ELEMENT( m_Timer, "Timer") UI_MAP_ELEMENT( m_Timer, "Timer")
if(m_loadedResolution == eSceneResolution_1080) UI_MAP_ELEMENT( m_labelXboxStore, "XboxLabel" )
{ UI_MAP_ELEMENT( m_buttonMSSignIn, "MSSignIn" )
UI_MAP_ELEMENT( m_labelXboxStore, "XboxLabel" )
}
UI_END_MAP_ELEMENTS_AND_NAMES() UI_END_MAP_ELEMENTS_AND_NAMES()
static int ExitDLCMainMenu(void *pParam,int iPad,C4JStorage::EMessageResult result); static int ExitDLCMainMenu(void *pParam,int iPad,C4JStorage::EMessageResult result);
bool m_authLabelUpdated = false; // stops tick() polling once label is set
#if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) #if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__)
bool m_bCategoriesShown; bool m_bCategoriesShown;
#endif #endif

View file

@ -819,7 +819,13 @@ void UIScene_HUD::repositionHud(S32 tileWidth, S32 tileHeight, F32 scale, bool n
S32 visibleW = static_cast<S32>(tileWidth / scale); S32 visibleW = static_cast<S32>(tileWidth / scale);
S32 visibleH = static_cast<S32>(tileHeight / scale); S32 visibleH = static_cast<S32>(tileHeight / scale);
app.DebugPrintf(app.USER_SR, "Reposition HUD: tile %dx%d, scale %.3f, visible SWF %dx%d\n", tileWidth, tileHeight, scale, visibleW, visibleH ); // Log only when values actually change to avoid per-frame spam.
static S32 s_lastVisibleW = 0, s_lastVisibleH = 0;
if (visibleW != s_lastVisibleW || visibleH != s_lastVisibleH) {
app.DebugPrintf(app.USER_SR, "Reposition HUD: tile %dx%d, scale %.3f, visible SWF %dx%d\n", tileWidth, tileHeight, scale, visibleW, visibleH );
s_lastVisibleW = visibleW;
s_lastVisibleH = visibleH;
}
IggyDataValue result; IggyDataValue result;
IggyDataValue value[2]; IggyDataValue value[2];

View file

@ -2,6 +2,9 @@
#include "UI.h" #include "UI.h"
#include "UIScene_LoadMenu.h" #include "UIScene_LoadMenu.h"
#include "..\..\Minecraft.h" #include "..\..\Minecraft.h"
#ifdef _WINDOWS64
#include "..\..\..\MCAuth\include\MCAuthManager.h"
#endif
#include "..\..\User.h" #include "..\..\User.h"
#include "..\..\TexturePackRepository.h" #include "..\..\TexturePackRepository.h"
#include "..\..\Options.h" #include "..\..\Options.h"
@ -1080,6 +1083,26 @@ void UIScene_LoadMenu::handleTimerComplete(int id)
void UIScene_LoadMenu::LaunchGame(void) void UIScene_LoadMenu::LaunchGame(void)
{ {
#ifdef _WINDOWS64
// Block world launch if no account is configured or session has no username.
// Open the auth picker so the player can sign in or create an offline account.
{
auto session = MCAuthManager::Get().GetSlotSession(0);
if (session.username.empty() || session.uuid.empty())
{
app.DebugPrintf("[Auth] No valid account (username='%s', uuid='%s'), returning to main menu\n",
session.username.c_str(), session.uuid.c_str());
m_bIgnoreInput = false;
// Go all the way back to the main menu then open the auth picker.
// MSAuth uses NativeUI (D3D overlay) which can't render on top of
// SWF scenes, so we must clear them first.
ui.NavigateToHomeMenu();
ui.NavigateToScene(m_iPad, eUIScene_MSAuth);
return;
}
}
#endif
// stop the timer running that causes a check for new texture packs in TMS but not installed, since this will run all through the load game, and will crash if it tries to create an hbrush // stop the timer running that causes a check for new texture packs in TMS but not installed, since this will run all through the load game, and will crash if it tries to create an hbrush
#ifdef _XBOX #ifdef _XBOX
killTimer(CHECKFORAVAILABLETEXTUREPACKS_TIMER_ID); killTimer(CHECKFORAVAILABLETEXTUREPACKS_TIMER_ID);
@ -1632,8 +1655,31 @@ void UIScene_LoadMenu::StartGameFromSave(UIScene_LoadMenu* pClass, DWORD dwLocal
#ifdef _WINDOWS64 #ifdef _WINDOWS64
{ {
extern wchar_t g_Win64UsernameW[17]; auto& mgr = MCAuthManager::Get();
Minecraft::GetInstance()->user->name = g_Win64UsernameW;
// Safety net: if TryRestoreActiveJavaAccount was never called (e.g. MainMenu
// constructor skipped), ensure we at least attempt account restoration now.
mgr.TryRestoreActiveJavaAccount();
// Check if auth is still refreshing — if so, wait briefly (non-blocking on main thread
// is impractical here since StartGameFromSave runs synchronously before the progress
// screen; cap the wait to avoid long UI freezes).
MCAuthManager::State st = mgr.GetSlotState(0);
if (st == MCAuthManager::State::Authenticating || st == MCAuthManager::State::WaitingForCode)
{
app.DebugPrintf("[Auth] Waiting for token refresh to complete before world entry...\n");
mgr.WaitForSlotReady(0, 3000);
app.DebugPrintf("[Auth] Auth settled: state=%d, IsLoggedIn=%d\n",
(int)mgr.GetSlotState(0), (int)mgr.IsSlotLoggedIn(0));
}
// Now the session is guaranteed to be populated (or failed — offline fallback)
if (mgr.IsSlotLoggedIn(0))
{
auto session = mgr.GetSlotSession(0);
if (!session.username.empty())
Minecraft::GetInstance()->user->name = std::wstring(session.username.begin(), session.username.end());
}
} }
#endif #endif
#ifndef _XBOX #ifndef _XBOX

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,158 @@
#pragma once
/*
* UIScene_MSAuth Microsoft Account Manager.
*
* Two views:
* 1. Account List shows saved accounts, select/remove/add.
* 2. Device Code device code flow for adding a new account.
*
* Native overlay (no SWF).
*/
#include "UIScene.h"
#include "NativeUIRenderer.h"
#include "../../../MCAuth/include/MCAuthManager.h"
#include <atomic>
#include <vector>
#include <string>
#include <unordered_map>
// Control IDs for UIScene_MSAuth focus list.
// File-scope so static render helpers in the .cpp can reference them directly.
namespace MSAuthUI
{
enum EControls {
eBtn_Back = 0,
eBtn_AddAccount = 1,
eBtn_AddOffline = 2,
eBtn_OfflineConfirm = 3,
eBtn_ConfirmYes = 4,
eBtn_ConfirmNo = 5,
eBtn_OfflineTextBox = 6,
eBtn_AddElyby = 7,
eBtn_ElybyUsername = 8,
eBtn_ElybyPassword = 9,
eBtn_ElybySignIn = 10,
eBtn_ElybyCancel = 11,
eBtn_Elyby2FACode = 12,
eBtn_Elyby2FASubmit = 13,
eBtn_Elyby2FACancel = 14,
eBtn_RemoveBase = 50, // 50 + index = remove button for account i
eAccountBase = 100, // 100 + index = account row for account i
eLink_URL = 200,
};
}
class UIScene_MSAuth : public UIScene
{
private:
// View mode
enum EView { eView_AccountList, eView_DeviceCode, eView_OfflineInput, eView_ElybyInput, eView_Elyby2FA };
EView m_view = eView_AccountList;
// Shared flags survive scene destruction (prevent UAF from async callback)
struct AuthFlags {
std::atomic<bool> done { false };
std::atomic<bool> success{ false };
std::atomic<bool> need2FA{ false };
};
std::shared_ptr<AuthFlags> m_authFlags = std::make_shared<AuthFlags>();
int m_closeCountdown = 0;
int m_spinnerTick = 0;
NativeUI::NineSlice m_panel;
NativeUI::NineSlice m_recessPanel;
bool m_panelLoaded = false;
NativeUI::FocusList m_focus;
std::string m_cachedUri;
// Cached account list (refreshed each tick)
std::vector<MCAuthManager::JavaAccountInfo> m_accounts;
int m_activeIdx = -1;
// Scroll offset for account list
int m_scrollOffset = 0;
void StartAddAccount();
void SwitchToAccountList();
void SwitchToDeviceCode();
void SwitchToOfflineInput();
void SwitchToElybyInput();
void SwitchToElyby2FA();
void ConfirmOfflineAccount();
void SubmitElybyLogin();
void SubmitElyby2FA();
// Offline username input state
std::string m_offlineUsername;
int m_offlineCursorBlink = 0;
bool m_textInputActive = false; // true = text box has focus and is accepting keyboard input
// Ely.by login state
std::string m_elybyUsername;
std::string m_elybyPassword;
std::string m_elyby2FACode;
// (2FA flag lives in m_authFlags->need2FA for async safety)
int m_elybyActiveField = 0; // 0=username, 1=password, 2=2fa code
// Target slot for splitscreen (0 = primary player, 1-3 = splitscreen).
// When > 0, account selection binds to that slot instead of slot 0.
int m_targetSlot = 0;
// Input guard: ignore input for the first N ticks after opening to avoid
// processing the button press that triggered the scene to open.
int m_inputGuardTicks = 6;
// Remove confirmation dialog (-1 = not showing, >=0 = account index pending removal)
int m_pendingRemoveIdx = -1;
std::string m_pendingRemoveUuid; // UUID captured at click time for stale-index safety
public:
// Shared buffer for virtual keyboard callback (public so the file-static
// callback function can access it; prevents UAF if scene is destroyed
// while the keyboard is open on a different UI group).
struct PendingKeyboardResult {
std::string value;
std::atomic<bool> ready{false};
std::atomic<bool> valid{true};
};
// Skin texture cache entry (public so static render helpers can access it)
struct SkinEntry {
std::atomic<int> textureId{-2}; // -2=not started, -1=downloading/failed, -3=file ready, >=0=texture ID
std::string filePath;
};
private:
std::shared_ptr<PendingKeyboardResult> m_pendingKBResult;
// Skin texture cache (key = UUID string)
std::unordered_map<std::string, std::shared_ptr<SkinEntry>> m_skinCache;
void EnsureSkinLoaded(const std::string& uuid);
public:
UIScene_MSAuth(int iPad, void* initData, UILayer* parentLayer);
~UIScene_MSAuth();
virtual EUIScene getSceneType() override { return eUIScene_MSAuth; }
virtual wstring getMoviePath() override { return L""; }
virtual bool hidesLowerScenes() override { return true; }
virtual bool blocksInput() override { return true; }
virtual bool hasFocus(int iPad) override { return bHasFocus; }
virtual bool needsReloaded() override { return false; }
virtual void updateTooltips() override;
virtual void tick() override;
virtual void render(S32 width, S32 height,
C4JRender::eViewportType viewport) override;
virtual void handleInput(int iPad, int key, bool repeat,
bool pressed, bool released,
bool& handled) override;
virtual void handlePress(F64 controlId, F64 childId) override;
#ifdef _WINDOWS64
virtual bool handleMouseClick(F32 x, F32 y) override;
#endif
};

View file

@ -1,4 +1,5 @@
#include "stdafx.h" #include "stdafx.h"
#include "../../../MCAuth/include/MCAuthManager.h"
#include "..\..\..\Minecraft.World\Mth.h" #include "..\..\..\Minecraft.World\Mth.h"
#include "..\..\..\Minecraft.World\StringHelpers.h" #include "..\..\..\Minecraft.World\StringHelpers.h"
#include "..\..\..\Minecraft.World\Random.h" #include "..\..\..\Minecraft.World\Random.h"
@ -43,16 +44,10 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
m_buttons[static_cast<int>(eControl_Leaderboards)].init(IDS_LEADERBOARDS,eControl_Leaderboards); m_buttons[static_cast<int>(eControl_Leaderboards)].init(IDS_LEADERBOARDS,eControl_Leaderboards);
m_buttons[static_cast<int>(eControl_Achievements)].init( (UIString)IDS_ACHIEVEMENTS,eControl_Achievements); m_buttons[static_cast<int>(eControl_Achievements)].init( (UIString)IDS_ACHIEVEMENTS,eControl_Achievements);
m_buttons[static_cast<int>(eControl_HelpAndOptions)].init(IDS_HELP_AND_OPTIONS,eControl_HelpAndOptions); m_buttons[static_cast<int>(eControl_HelpAndOptions)].init(IDS_HELP_AND_OPTIONS,eControl_HelpAndOptions);
if(ProfileManager.IsFullVersion()) m_bTrialVersion = !ProfileManager.IsFullVersion();
{ // Try to restore a saved auth session (loads accounts + refreshes active in background).
m_bTrialVersion=false; MCAuthManager::Get().TryRestoreActiveJavaAccount();
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(IDS_DOWNLOADABLECONTENT,eControl_UnlockOrDLC); m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(L"Account Manager", eControl_UnlockOrDLC);
}
else
{
m_bTrialVersion=true;
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC);
}
#ifndef _DURANGO #ifndef _DURANGO
m_buttons[static_cast<int>(eControl_Exit)].init(app.GetString(IDS_EXIT_GAME),eControl_Exit); m_buttons[static_cast<int>(eControl_Exit)].init(app.GetString(IDS_EXIT_GAME),eControl_Exit);
@ -181,8 +176,7 @@ void UIScene_MainMenu::handleGainFocus(bool navBack)
if(navBack && ProfileManager.IsFullVersion()) if(navBack && ProfileManager.IsFullVersion())
{ {
// Replace the Unlock Full Game with Downloadable Content m_buttons[static_cast<int>(eControl_UnlockOrDLC)].setLabel(L"Account Manager");
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT);
} }
#if TO_BE_IMPLEMENTED #if TO_BE_IMPLEMENTED
@ -355,9 +349,7 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId)
case eControl_UnlockOrDLC: case eControl_UnlockOrDLC:
//CD - Added for audio //CD - Added for audio
ui.PlayUISFX(eSFX_Press); ui.PlayUISFX(eSFX_Press);
ui.NavigateToScene(m_iPad, eUIScene_MSAuth);
m_eAction=eAction_RunUnlockOrDLC;
signInReturnedFunc = &UIScene_MainMenu::UnlockFullGame_SignInReturned;
break; break;
case eControl_Exit: case eControl_Exit:
//CD - Added for audio //CD - Added for audio
@ -1858,6 +1850,7 @@ void UIScene_MainMenu::tick()
{ {
UIScene::tick(); UIScene::tick();
if ( (eNavigateWhenReady >= 0) ) if ( (eNavigateWhenReady >= 0) )
{ {
@ -2129,7 +2122,7 @@ void UIScene_MainMenu::LoadTrial(void)
void UIScene_MainMenu::handleUnlockFullVersion() void UIScene_MainMenu::handleUnlockFullVersion()
{ {
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT,true); m_buttons[static_cast<int>(eControl_UnlockOrDLC)].setLabel(L"Account Manager", true);
} }

View file

@ -2,6 +2,9 @@
#include "UI.h" #include "UI.h"
#include "UIScene_QuadrantSignin.h" #include "UIScene_QuadrantSignin.h"
#include "..\..\Minecraft.h" #include "..\..\Minecraft.h"
#ifdef _WINDOWS64
#include "..\..\..\MCAuth\include\MCAuthManager.h"
#endif
#if defined(__ORBIS__) #if defined(__ORBIS__)
#include "Common\Network\Sony\SonyHttp.h" #include "Common\Network\Sony\SonyHttp.h"
#endif #endif
@ -122,6 +125,23 @@ void UIScene_QuadrantSignin::handleInput(int iPad, int key, bool repeat, bool pr
app.DebugPrintf("Signed in pad pressed\n"); app.DebugPrintf("Signed in pad pressed\n");
ProfileManager.CancelProfileAvatarRequest(); ProfileManager.CancelProfileAvatarRequest();
#ifdef _WINDOWS64
// Block if no MCAuth account is configured (empty username/uuid).
// Redirect to the auth picker instead of proceeding with the join.
{
auto session = MCAuthManager::Get().GetSlotSession(0);
if (session.username.empty() || session.uuid.empty())
{
app.DebugPrintf("[Auth] No account configured, redirecting to auth picker\n");
m_bIgnoreInput = false;
navigateBack();
ui.NavigateToHomeMenu();
ui.NavigateToScene(m_iPad, eUIScene_MSAuth);
break;
}
}
#endif
#ifdef _XBOX_ONE #ifdef _XBOX_ONE
// On Durango, if we don't navigate forward here, then when we are on the main menu, it (re)gains focus & that causes our users to get cleared // On Durango, if we don't navigate forward here, then when we are on the main menu, it (re)gains focus & that causes our users to get cleared
ui.NavigateToScene(m_iPad, eUIScene_Timer); ui.NavigateToScene(m_iPad, eUIScene_Timer);

View file

@ -24,6 +24,7 @@ const WCHAR *UIScene_SkinSelectMenu::wchDefaultNamesA[]=
L"Prisoner Steve", L"Prisoner Steve",
L"Cyclist Steve", L"Cyclist Steve",
L"Boxer Steve", L"Boxer Steve",
L"Mojang Skin",
}; };
UIScene_SkinSelectMenu::UIScene_SkinSelectMenu(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer) UIScene_SkinSelectMenu::UIScene_SkinSelectMenu(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer)
@ -407,8 +408,16 @@ void UIScene_SkinSelectMenu::InputActionOK(unsigned int iPad)
switch(m_packIndex) switch(m_packIndex)
{ {
case SKIN_SELECT_PACK_DEFAULT: case SKIN_SELECT_PACK_DEFAULT:
app.SetPlayerSkin(iPad, m_skinIndex); if (m_skinIndex == eDefaultSkins_MojangSkin && !app.m_mojangSkinKey[m_iPad].empty())
app.SetPlayerCape(iPad, 0); {
app.SetPlayerMojangSkin(iPad);
app.SetPlayerCape(iPad, 0);
}
else
{
app.SetPlayerSkin(iPad, m_skinIndex);
app.SetPlayerCape(iPad, 0);
}
m_currentSkinPath = app.GetPlayerSkinName(iPad); m_currentSkinPath = app.GetPlayerSkinName(iPad);
m_originalSkinId = app.GetPlayerSkinId(iPad); m_originalSkinId = app.GetPlayerSkinId(iPad);
setCharacterSelected(true); setCharacterSelected(true);
@ -684,6 +693,15 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged()
{ {
skinName = app.GetString(IDS_DEFAULT_SKINS); skinName = app.GetString(IDS_DEFAULT_SKINS);
} }
else if( m_skinIndex == eDefaultSkins_MojangSkin )
{
skinName = wchDefaultNamesA[m_skinIndex];
// Use the Mojang skin from memory textures
if (!app.m_mojangSkinKey[m_iPad].empty())
{
m_selectedSkinPath = app.m_mojangSkinKey[m_iPad];
}
}
else else
{ {
skinName = wchDefaultNamesA[m_skinIndex]; skinName = wchDefaultNamesA[m_skinIndex];
@ -693,10 +711,16 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged()
{ {
setCharacterSelected(true); setCharacterSelected(true);
} }
// Hide Mojang skin entry if no Mojang skin is available
if( m_skinIndex == eDefaultSkins_MojangSkin && app.m_mojangSkinKey[m_iPad].empty() )
{
m_characters[eCharacter_Current].setVisible(false);
}
else
{
m_characters[eCharacter_Current].setVisible(true);
}
setCharacterLocked(false); setCharacterLocked(false);
setCharacterLocked(false);
m_characters[eCharacter_Current].setVisible(true);
m_controlSkinNamePlate.setVisible( true ); m_controlSkinNamePlate.setVisible( true );
break; break;
@ -845,6 +869,8 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged()
{ {
case SKIN_SELECT_PACK_DEFAULT: case SKIN_SELECT_PACK_DEFAULT:
backupTexture = getTextureId(nextIndex); backupTexture = getTextureId(nextIndex);
if(nextIndex == eDefaultSkins_MojangSkin && !app.m_mojangSkinKey[m_iPad].empty())
otherSkinPath = app.m_mojangSkinKey[m_iPad];
break; break;
case SKIN_SELECT_PACK_FAVORITES: case SKIN_SELECT_PACK_FAVORITES:
if(uiCurrentFavoriteC>0) if(uiCurrentFavoriteC>0)
@ -916,6 +942,8 @@ void UIScene_SkinSelectMenu::handleSkinIndexChanged()
{ {
case SKIN_SELECT_PACK_DEFAULT: case SKIN_SELECT_PACK_DEFAULT:
backupTexture = getTextureId(previousIndex); backupTexture = getTextureId(previousIndex);
if(previousIndex == eDefaultSkins_MojangSkin && !app.m_mojangSkinKey[m_iPad].empty())
otherSkinPath = app.m_mojangSkinKey[m_iPad];
break; break;
case SKIN_SELECT_PACK_FAVORITES: case SKIN_SELECT_PACK_FAVORITES:
if(uiCurrentFavoriteC>0) if(uiCurrentFavoriteC>0)
@ -993,6 +1021,9 @@ TEXTURE_NAME UIScene_SkinSelectMenu::getTextureId(int skinIndex)
case eDefaultSkins_Skin7: case eDefaultSkins_Skin7:
texture = TN_MOB_CHAR7; texture = TN_MOB_CHAR7;
break; break;
case eDefaultSkins_MojangSkin:
texture = TN_MOB_CHAR; // fallback if memory texture not loaded yet
break;
}; };
return texture; return texture;
@ -1017,9 +1048,13 @@ int UIScene_SkinSelectMenu::getNextSkinIndex(DWORD sourceIndex)
default: default:
++nextSkin; ++nextSkin;
if(m_packIndex == SKIN_SELECT_PACK_DEFAULT && nextSkin >= eDefaultSkins_Count) if(m_packIndex == SKIN_SELECT_PACK_DEFAULT)
{ {
nextSkin = eDefaultSkins_ServerSelected; // Skip Mojang skin entry if no Mojang skin is available
if(nextSkin == eDefaultSkins_MojangSkin && app.m_mojangSkinKey[m_iPad].empty())
++nextSkin;
if(nextSkin >= eDefaultSkins_Count)
nextSkin = eDefaultSkins_ServerSelected;
} }
else if(m_currentPack != nullptr && nextSkin>=m_currentPack->getSkinCount()) else if(m_currentPack != nullptr && nextSkin>=m_currentPack->getSkinCount())
{ {
@ -1054,6 +1089,9 @@ int UIScene_SkinSelectMenu::getPreviousSkinIndex(DWORD sourceIndex)
if(m_packIndex == SKIN_SELECT_PACK_DEFAULT) if(m_packIndex == SKIN_SELECT_PACK_DEFAULT)
{ {
previousSkin = eDefaultSkins_Count - 1; previousSkin = eDefaultSkins_Count - 1;
// Skip Mojang skin entry if no Mojang skin is available
if(previousSkin == eDefaultSkins_MojangSkin && app.m_mojangSkinKey[m_iPad].empty())
--previousSkin;
} }
else if(m_currentPack != nullptr) else if(m_currentPack != nullptr)
{ {
@ -1063,6 +1101,9 @@ int UIScene_SkinSelectMenu::getPreviousSkinIndex(DWORD sourceIndex)
else else
{ {
--previousSkin; --previousSkin;
// Skip Mojang skin entry if no Mojang skin is available
if(m_packIndex == SKIN_SELECT_PACK_DEFAULT && previousSkin == eDefaultSkins_MojangSkin && app.m_mojangSkinKey[m_iPad].empty())
--previousSkin;
} }
break; break;
} }

View file

@ -478,7 +478,7 @@ bool CPlatformNetworkManagerDurango::_LeaveGame(bool bMigrateHost, bool bLeaveRo
return true; return true;
} }
void CPlatformNetworkManagerDurango::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerDurango::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
// #ifdef _XBOX // #ifdef _XBOX
// 4J Stu - We probably did this earlier as well, but just to be sure! // 4J Stu - We probably did this earlier as well, but just to be sure!
@ -495,7 +495,7 @@ void CPlatformNetworkManagerDurango::HostGame(int localUsersMask, bool bOnlineGa
//#endif //#endif
} }
void CPlatformNetworkManagerDurango::_HostGame(int usersMask, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerDurango::_HostGame(int usersMask, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
memset(&m_hostGameSessionData,0,sizeof(m_hostGameSessionData)); memset(&m_hostGameSessionData,0,sizeof(m_hostGameSessionData));
m_hostGameSessionData.netVersion = MINECRAFT_NET_VERSION; m_hostGameSessionData.netVersion = MINECRAFT_NET_VERSION;

View file

@ -44,7 +44,7 @@ public:
virtual void SendInviteGUI(int quadrant); virtual void SendInviteGUI(int quadrant);
virtual bool IsAddingPlayer(); virtual bool IsAddingPlayer();
virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex ); virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex );
virtual void CancelJoinGame(); virtual void CancelJoinGame();
virtual bool SetLocalGame(bool isLocal); virtual bool SetLocalGame(bool isLocal);
@ -64,7 +64,7 @@ public:
private: private:
bool isSystemPrimaryPlayer(DQRNetworkPlayer *pDQRPlayer); bool isSystemPrimaryPlayer(DQRNetworkPlayer *pDQRPlayer);
virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom); virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom);
virtual void _HostGame(int dwUsersMask, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void _HostGame(int dwUsersMask, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual bool _StartGame(); virtual bool _StartGame();
DQRNetworkManager * m_pDQRNet; // pointer to SQRNetworkManager interface DQRNetworkManager * m_pDQRNet; // pointer to SQRNetworkManager interface

View file

@ -19,7 +19,7 @@ public:
WCHAR wNameXUID[32] = L""; WCHAR wNameXUID[32] = L"";
WCHAR wNameSkin[32] = L""; WCHAR wNameSkin[32] = L"";
WCHAR wNameCloak[32] = L""; WCHAR wNameCloak[32] = L"";
PlayerUID xuid=0LL; PlayerUID xuid = INVALID_XUID;
if (NameLen >31) if (NameLen >31)
@ -47,7 +47,11 @@ public:
{ {
ZeroMemory(wTemp,sizeof(WCHAR)*35); ZeroMemory(wTemp,sizeof(WCHAR)*35);
wcsncpy_s( wTemp, pAttributes[i].strValue, pAttributes[i].ValueLen); wcsncpy_s( wTemp, pAttributes[i].strValue, pAttributes[i].ValueLen);
xuid=_wcstoui64(wTemp,nullptr,10); {
char narrow[64] = {};
wcstombs_s(nullptr, narrow, wTemp, _TRUNCATE);
xuid = PlayerUID::fromDashed(std::string(narrow));
}
} }
} }
else if (_wcsicmp(wAttName,L"cape")==0) else if (_wcsicmp(wAttName,L"cape")==0)
@ -68,7 +72,7 @@ public:
} }
// if the xuid hasn't been defined, then we can't use the data // if the xuid hasn't been defined, then we can't use the data
if(xuid!=0LL) if(xuid.isValid())
{ {
return CConsoleMinecraftApp::RegisterMojangData(wNameXUID , xuid, wNameSkin, wNameCloak ); return CConsoleMinecraftApp::RegisterMojangData(wNameXUID , xuid, wNameSkin, wNameCloak );
} }

View file

@ -21,7 +21,8 @@
#include "Windows64\Social\SocialManager.h" #include "Windows64\Social\SocialManager.h"
#include "Windows64\Sentient\DynamicConfigurations.h" #include "Windows64\Sentient\DynamicConfigurations.h"
#include "Windows64\Network\WinsockNetLayer.h" #include "Windows64\Network\WinsockNetLayer.h"
#include "Windows64\Windows64_Xuid.h" #include "Windows64\Windows64_Uuid.h"
#include "..\MCAuth\include\MCAuthManager.h"
#elif defined __PSVITA__ #elif defined __PSVITA__
#include "PSVita\Sentient\SentientManager.h" #include "PSVita\Sentient\SentientManager.h"
#include "StatsCounter.h" #include "StatsCounter.h"
@ -172,11 +173,7 @@ void PIXSetMarkerDeprecated(int a, const char* b, ...) {}
bool IsEqualXUID(PlayerUID a, PlayerUID b) bool IsEqualXUID(PlayerUID a, PlayerUID b)
{ {
#if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) || defined(_DURANGO)
return (a == b); return (a == b);
#else
return false;
#endif
} }
void XMemCpy(void* a, const void* b, size_t s) { memcpy(a, b, s); } void XMemCpy(void* a, const void* b, size_t s) { memcpy(a, b, s); }
@ -214,12 +211,7 @@ bool IQNetPlayer::IsGuest() { return false; }
bool IQNetPlayer::IsLocal() { return !m_isRemote; } bool IQNetPlayer::IsLocal() { return !m_isRemote; }
PlayerUID IQNetPlayer::GetXuid() PlayerUID IQNetPlayer::GetXuid()
{ {
// Compatibility model: return m_resolvedXuid;
// - Preferred path: use per-player resolved XUID populated from login/add-player flow.
// - Fallback path: keep legacy base+smallId behavior for peers/saves still on old scheme.
if (m_resolvedXuid != INVALID_XUID)
return m_resolvedXuid;
return (PlayerUID)(0xe000d45248242f2e + m_smallId);
} }
LPCWSTR IQNetPlayer::GetGamertag() { return m_gamertag; } LPCWSTR IQNetPlayer::GetGamertag() { return m_gamertag; }
int IQNetPlayer::GetSessionIndex() { return m_smallId; } int IQNetPlayer::GetSessionIndex() { return m_smallId; }
@ -254,16 +246,31 @@ void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost
static bool Win64_IsActivePlayer(IQNetPlayer* p, DWORD index); static bool Win64_IsActivePlayer(IQNetPlayer* p, DWORD index);
static std::wstring GetAuthUsername(int slot)
{
auto& mgr = MCAuthManager::Get();
if (mgr.IsSlotLoggedIn(slot))
{
auto session = mgr.GetSlotSession(slot);
if (!session.username.empty())
return std::wstring(session.username.begin(), session.username.end());
}
if (slot > 0)
{
wchar_t buf[32];
swprintf_s(buf, 32, L"Player(%d)", slot + 1);
return buf;
}
return L"Player";
}
HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex) { HRESULT IQNet::AddLocalPlayerByUserIndex(DWORD dwUserIndex) {
if (dwUserIndex >= MINECRAFT_NET_MAX_PLAYERS) return E_FAIL; if (dwUserIndex >= MINECRAFT_NET_MAX_PLAYERS) return E_FAIL;
m_player[dwUserIndex].m_isRemote = false; m_player[dwUserIndex].m_isRemote = false;
m_player[dwUserIndex].m_isHostPlayer = false; m_player[dwUserIndex].m_isHostPlayer = false;
// Give the joining player a distinct gamertag // Give the joining player the auth username for their slot
extern wchar_t g_Win64UsernameW[17]; std::wstring authName = GetAuthUsername(dwUserIndex);
if (dwUserIndex == 0) wcscpy_s(m_player[dwUserIndex].m_gamertag, 32, authName.c_str());
wcscpy_s(m_player[0].m_gamertag, 32, g_Win64UsernameW);
else
swprintf_s(m_player[dwUserIndex].m_gamertag, 32, L"%s(%d)", g_Win64UsernameW, dwUserIndex + 1);
if (dwUserIndex >= s_playerCount) if (dwUserIndex >= s_playerCount)
s_playerCount = dwUserIndex + 1; s_playerCount = dwUserIndex + 1;
return S_OK; return S_OK;
@ -323,7 +330,10 @@ IQNetPlayer* IQNet::GetPlayerByIndex(DWORD dwPlayerIndex)
found++; found++;
} }
} }
return &m_player[0]; // Don't silently fall back to player[0] — callers must handle nullptr.
// Returning player[0] here caused the TAB list to show duplicates:
// any out-of-range index would alias to the host player.
return nullptr;
} }
IQNetPlayer* IQNet::GetPlayerBySmallId(BYTE SmallId) IQNetPlayer* IQNet::GetPlayerBySmallId(BYTE SmallId)
{ {
@ -335,6 +345,11 @@ IQNetPlayer* IQNet::GetPlayerBySmallId(BYTE SmallId)
} }
IQNetPlayer* IQNet::GetPlayerByXuid(PlayerUID xuid) IQNetPlayer* IQNet::GetPlayerByXuid(PlayerUID xuid)
{ {
// Guard against INVALID_XUID lookups — every unresolved slot has INVALID_XUID,
// so searching for it would alias to the first active slot (usually the host).
if (xuid == INVALID_XUID)
return nullptr;
for (DWORD i = 0; i < s_playerCount; i++) for (DWORD i = 0; i < s_playerCount; i++)
{ {
if (!Win64_IsActivePlayer(&m_player[i], i)) if (!Win64_IsActivePlayer(&m_player[i], i))
@ -343,8 +358,7 @@ IQNetPlayer* IQNet::GetPlayerByXuid(PlayerUID xuid)
if (m_player[i].GetXuid() == xuid) if (m_player[i].GetXuid() == xuid)
return &m_player[i]; return &m_player[i];
} }
// Keep existing stub behavior: return host slot instead of nullptr on miss. return nullptr;
return &m_player[0];
} }
DWORD IQNet::GetPlayerCount() DWORD IQNet::GetPlayerCount()
{ {
@ -362,8 +376,7 @@ void IQNet::HostGame()
{ {
_iQNetStubState = QNET_STATE_SESSION_STARTING; _iQNetStubState = QNET_STATE_SESSION_STARTING;
s_isHosting = true; s_isHosting = true;
// Host slot keeps legacy XUID so old host player data remains addressable. m_player[0].m_resolvedXuid = INVALID_XUID; // Will be set by auth handshake
m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid();
} }
void IQNet::ClientJoinGame() void IQNet::ClientJoinGame()
{ {
@ -394,10 +407,9 @@ void IQNet::EndGame()
m_player[i].m_gamertag[0] = 0; m_player[i].m_gamertag[0] = 0;
m_player[i].SetCustomDataValue(0); m_player[i].SetCustomDataValue(0);
} }
// Restore local player 0's gamertag so re-joining works correctly
extern wchar_t g_Win64UsernameW[17];
m_player[0].m_isHostPlayer = true; m_player[0].m_isHostPlayer = true;
wcscpy_s(m_player[0].m_gamertag, 32, g_Win64UsernameW); std::wstring authName0 = GetAuthUsername(0);
wcscpy_s(m_player[0].m_gamertag, 32, authName0.c_str());
} }
DWORD MinecraftDynamicConfigurations::GetTrialTime() { return DYNAMIC_CONFIG_DEFAULT_TRIAL_TIME; } DWORD MinecraftDynamicConfigurations::GetTrialTime() { return DYNAMIC_CONFIG_DEFAULT_TRIAL_TIME; }
@ -634,14 +646,8 @@ void C_4JProfile::SetPrimaryPlayerChanged(bool bVal) {}
bool C_4JProfile::QuerySigninStatus(void) { return true; } bool C_4JProfile::QuerySigninStatus(void) { return true; }
void C_4JProfile::GetXUID(int iPad, PlayerUID * pXuid, bool bOnlineXuid) void C_4JProfile::GetXUID(int iPad, PlayerUID * pXuid, bool bOnlineXuid)
{ {
#ifdef _WINDOWS64 // Stub — auth manager assigns UUID during login
// Each pad gets a unique XUID derived from the persistent uid.dat value. *pXuid = INVALID_XUID;
// Pad 0 uses the base XUID directly. Pads 1-3 get a deterministic hash
// of (base + pad) to produce fully independent IDs with no overlap risk.
*pXuid = Win64Xuid::DeriveXuidForPad(Win64Xuid::ResolvePersistentXuid(), iPad);
#else
* pXuid = 0xe000d45248242f2e + iPad;
#endif
} }
BOOL C_4JProfile::AreXUIDSEqual(PlayerUID xuid1, PlayerUID xuid2) { return xuid1 == xuid2; } BOOL C_4JProfile::AreXUIDSEqual(PlayerUID xuid1, PlayerUID xuid2) { return xuid1 == xuid2; }
BOOL C_4JProfile::XUIDIsGuest(PlayerUID xuid) { return false; } BOOL C_4JProfile::XUIDIsGuest(PlayerUID xuid) { return false; }
@ -669,7 +675,6 @@ char fakeGamerTag[32] = "PlayerName";
void SetFakeGamertag(char* name) { strcpy_s(fakeGamerTag, name); } void SetFakeGamertag(char* name) { strcpy_s(fakeGamerTag, name); }
#else #else
char* C_4JProfile::GetGamertag(int iPad) { char* C_4JProfile::GetGamertag(int iPad) {
extern char g_Win64Username[17];
if (iPad > 0 && iPad < XUSER_MAX_COUNT && IQNet::m_player[iPad].m_gamertag[0] != 0 && if (iPad > 0 && iPad < XUSER_MAX_COUNT && IQNet::m_player[iPad].m_gamertag[0] != 0 &&
!IQNet::m_player[iPad].m_isRemote) !IQNet::m_player[iPad].m_isRemote)
{ {
@ -677,14 +682,33 @@ char* C_4JProfile::GetGamertag(int iPad) {
WideCharToMultiByte(CP_ACP, 0, IQNet::m_player[iPad].m_gamertag, -1, s_padGamertag[iPad], 17, nullptr, nullptr); WideCharToMultiByte(CP_ACP, 0, IQNet::m_player[iPad].m_gamertag, -1, s_padGamertag[iPad], 17, nullptr, nullptr);
return s_padGamertag[iPad]; return s_padGamertag[iPad];
} }
return g_Win64Username; // MCAuth session is the source-of-truth for player names
static char s_authGamertag[XUSER_MAX_COUNT][17];
auto& mgr = MCAuthManager::Get();
if (mgr.IsSlotLoggedIn(iPad))
{
auto session = mgr.GetSlotSession(iPad);
if (!session.username.empty())
{
strncpy_s(s_authGamertag[iPad], sizeof(s_authGamertag[iPad]), session.username.c_str(), _TRUNCATE);
return s_authGamertag[iPad];
}
}
static char s_defaultTag[] = "Player";
return s_defaultTag;
} }
wstring C_4JProfile::GetDisplayName(int iPad) { wstring C_4JProfile::GetDisplayName(int iPad) {
extern wchar_t g_Win64UsernameW[17];
if (iPad > 0 && iPad < XUSER_MAX_COUNT && IQNet::m_player[iPad].m_gamertag[0] != 0 && if (iPad > 0 && iPad < XUSER_MAX_COUNT && IQNet::m_player[iPad].m_gamertag[0] != 0 &&
!IQNet::m_player[iPad].m_isRemote) !IQNet::m_player[iPad].m_isRemote)
return IQNet::m_player[iPad].m_gamertag; return IQNet::m_player[iPad].m_gamertag;
return g_Win64UsernameW; auto& mgr = MCAuthManager::Get();
if (mgr.IsSlotLoggedIn(iPad))
{
auto session = mgr.GetSlotSession(iPad);
if (!session.username.empty())
return std::wstring(session.username.begin(), session.username.end());
}
return L"Player";
} }
#endif #endif
bool C_4JProfile::IsFullVersion() { return s_bProfileIsFullVersion; } bool C_4JProfile::IsFullVersion() { return s_bProfileIsFullVersion; }

View file

@ -91,6 +91,32 @@ public:
void setEnforceUnicodeSheet(bool enforceUnicodeSheet); void setEnforceUnicodeSheet(bool enforceUnicodeSheet);
void setBidirectional(bool bidirectional); void setBidirectional(bool bidirectional);
// Bind the font atlas texture so external code (e.g. NativeUI) can sample
// the white texel at UV (0,0) for solid-colour rectangles.
void bindFontTexture() { textures->bindTexture(m_textureLocation); }
// Expose font metrics for NativeUI D3D11 text renderer.
int getCols() const { return m_cols; }
int getRows() const { return m_rows; }
int getCharWidth() const { return m_charWidth; }
int getCharHeight() const { return m_charHeight; }
int getCharPixelWidth(wchar_t c) const { return charWidths[MapCharacterConst(c)]; }
wchar_t mapChar(wchar_t c) const { return static_cast<wchar_t>(MapCharacterConst(c)); }
ResourceLocation* getTextureLocation() const { return m_textureLocation; }
Textures* getTextures() const { return textures; }
private:
int MapCharacterConst(wchar_t c) const
{
if (!m_charMap.empty() && c != L' ')
{
auto it = m_charMap.find(c);
return (it != m_charMap.end()) ? it->second : 0;
}
return c;
}
public:
// 4J-PB - check for invalid player name - Japanese local name // 4J-PB - check for invalid player name - Japanese local name
bool AllCharactersValid(const wstring &str); bool AllCharactersValid(const wstring &str);
}; };

View file

@ -88,7 +88,7 @@ LocalPlayer::LocalPlayer(Minecraft *minecraft, Level *level, User *user, int dim
this->name = user->name; this->name = user->name;
//wprintf(L"Created LocalPlayer with name %ls\n", name.c_str() ); //wprintf(L"Created LocalPlayer with name %ls\n", name.c_str() );
// check to see if this player's xuid is in the list of special players // check to see if this player's xuid is in the list of special players
MOJANG_DATA *pMojangData=app.GetMojangDataForXuid(getOnlineXuid()); MOJANG_DATA *pMojangData=app.GetMojangDataForXuid(getXuid());
if(pMojangData) if(pMojangData)
{ {
customTextureUrl=pMojangData->wchSkin; customTextureUrl=pMojangData->wchSkin;

View file

@ -1,5 +1,8 @@
#include "stdafx.h" #include "stdafx.h"
#include "Minecraft.h" #include "Minecraft.h"
#ifdef _WINDOWS64
#include "..\MCAuth\include\MCAuthManager.h"
#endif
#include "Common/UI/UIScene.h" #include "Common/UI/UIScene.h"
#include "GameMode.h" #include "GameMode.h"
#include "Timer.h" #include "Timer.h"
@ -54,7 +57,7 @@
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h" #include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
#include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\net.minecraft.world.item.h"
#include "..\Minecraft.World\Minecraft.World.h" #include "..\Minecraft.World\Minecraft.World.h"
#include "Windows64\Windows64_Xuid.h" #include "Windows64\Windows64_Uuid.h"
#include "ClientConnection.h" #include "ClientConnection.h"
#include "..\Minecraft.World\HellRandomLevelSource.h" #include "..\Minecraft.World\HellRandomLevelSource.h"
#include "..\Minecraft.World\net.minecraft.world.entity.animal.h" #include "..\Minecraft.World\net.minecraft.world.entity.animal.h"
@ -1040,25 +1043,8 @@ shared_ptr<MultiplayerLocalPlayer> Minecraft::createExtraLocalPlayer(int idx, co
//localitemInHandRenderers[idx] = new ItemInHandRenderer(this); //localitemInHandRenderers[idx] = new ItemInHandRenderer(this);
localplayers[idx] = localgameModes[idx]->createPlayer(level); localplayers[idx] = localgameModes[idx]->createPlayer(level);
PlayerUID playerXUIDOffline = INVALID_XUID; PlayerUID playerXUID = GameUUID::generateOffline(std::string(localplayers[idx]->name.begin(), localplayers[idx]->name.end()));
PlayerUID playerXUIDOnline = INVALID_XUID; localplayers[idx]->setXuid(playerXUID);
ProfileManager.GetXUID(idx,&playerXUIDOffline,false);
ProfileManager.GetXUID(idx,&playerXUIDOnline,true);
#ifdef _WINDOWS64
// Compatibility rule for Win64 id migration
// host keeps legacy host XUID, non-host uses persistent uid.dat XUID.
INetworkPlayer *localNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(idx);
if(localNetworkPlayer != nullptr && localNetworkPlayer->IsHost())
{
playerXUIDOffline = Win64Xuid::GetLegacyEmbeddedHostXuid();
}
else
{
playerXUIDOffline = Win64Xuid::ResolvePersistentXuid();
}
#endif
localplayers[idx]->setXuid(playerXUIDOffline);
localplayers[idx]->setOnlineXuid(playerXUIDOnline);
localplayers[idx]->setIsGuest(ProfileManager.IsGuest(idx)); localplayers[idx]->setIsGuest(ProfileManager.IsGuest(idx));
localplayers[idx]->m_displayName = ProfileManager.GetDisplayName(idx); localplayers[idx]->m_displayName = ProfileManager.GetDisplayName(idx);
@ -1110,7 +1096,7 @@ void Minecraft::storeExtraLocalPlayer(int idx)
void Minecraft::removeLocalPlayerIdx(int idx) void Minecraft::removeLocalPlayerIdx(int idx)
{ {
bool updateXui = true; bool updateXui = true;
if(localgameModes[idx] != nullptr) if(localgameModes[idx] != nullptr && localplayers[idx] != nullptr)
{ {
if( getLevel( localplayers[idx]->dimension )->isClientSide ) if( getLevel( localplayers[idx]->dimension )->isClientSide )
{ {
@ -1156,6 +1142,13 @@ void Minecraft::removeLocalPlayerIdx(int idx)
} }
localplayers[idx] = nullptr; localplayers[idx] = nullptr;
#ifdef _WINDOWS64
// Reset splitscreen join state so the slot can be reused
resetSplitJoinState(idx);
// Release the auth account slot so it's no longer shown as "Player N" in the picker
MCAuthManager::Get().ClearSlot(idx);
#endif
if( idx == ProfileManager.GetPrimaryPad() ) if( idx == ProfileManager.GetPrimaryPad() )
{ {
// We should never try to remove the Primary player in this way // We should never try to remove the Primary player in this way
@ -1189,7 +1182,25 @@ void Minecraft::createPrimaryLocalPlayer(int iPad)
localgameModes[iPad] = gameMode; localgameModes[iPad] = gameMode;
localplayers[iPad] = player; localplayers[iPad] = player;
//gameRenderer->itemInHandRenderer = localitemInHandRenderers[iPad]; //gameRenderer->itemInHandRenderer = localitemInHandRenderers[iPad];
// Give them the gamertag if they're signed in
#ifdef _WINDOWS64
// Auth handshake already set user->name; don't overwrite.
{
auto& mgr = MCAuthManager::Get();
if (mgr.IsSlotLoggedIn(iPad))
{
auto session = mgr.GetSlotSession(iPad);
if (!session.username.empty())
{
// Only update user->name for primary player (it's a shared global)
if (iPad == 0)
user->name = std::wstring(session.username.begin(), session.username.end());
return;
}
}
}
#endif
// Fallback: use ProfileManager gamertag (console platforms, or no auth session)
if(ProfileManager.IsSignedIn(ProfileManager.GetPrimaryPad())) if(ProfileManager.IsSignedIn(ProfileManager.GetPrimaryPad()))
{ {
user->name = convStringToWstring( ProfileManager.GetGamertag(ProfileManager.GetPrimaryPad()) ); user->name = convStringToWstring( ProfileManager.GetGamertag(ProfileManager.GetPrimaryPad()) );
@ -1203,14 +1214,19 @@ void Minecraft::applyFrameMouseLook()
// for the 20Hz game tick. Apply the same delta to both xRot/yRot AND xRotO/yRotO // for the 20Hz game tick. Apply the same delta to both xRot/yRot AND xRotO/yRotO
// so the render interpolation instantly reflects the change without waiting for a tick. // so the render interpolation instantly reflects the change without waiting for a tick.
if (level == nullptr) return; if (level == nullptr) return;
if (!g_KBMInput.IsMouseGrabbed()) return;
// If any pad has a blocking UI open, skip mouse look but do NOT drain
// the delta accumulators — they are used by KeyboardMouseInput::Tick()
// to populate m_mouseDeltaX/Y which the inventory cursor reads.
for (int p = 0; p < XUSER_MAX_COUNT; ++p)
if (ui.GetMenuDisplayed(p)) return;
for (int i = 0; i < XUSER_MAX_COUNT; i++) for (int i = 0; i < XUSER_MAX_COUNT; i++)
{ {
if (localplayers[i] == nullptr) continue; if (localplayers[i] == nullptr) continue;
int iPad = localplayers[i]->GetXboxPad(); int iPad = localplayers[i]->GetXboxPad();
if (iPad != 0) continue; // Mouse only applies to pad 0 if (iPad != 0) continue; // Mouse only applies to pad 0
if (!g_KBMInput.IsMouseGrabbed()) continue;
if (localgameModes[iPad] == nullptr) continue; if (localgameModes[iPad] == nullptr) continue;
float rawDx, rawDy; float rawDx, rawDy;
@ -1629,6 +1645,46 @@ void Minecraft::run_middle()
s_prevXButtons[i] = xCurButtons; s_prevXButtons[i] = xCurButtons;
} }
bool startJustPressed = s_startPressLatch[i] > 0; bool startJustPressed = s_startPressLatch[i] > 0;
// State machine: block the join flow when auth UI is open
if (m_splitJoinState[i] == ESplitJoinState::AuthUI)
{
s_startPressLatch[i] = 0;
continue;
}
// AuthDone: account selected, join immediately without waiting for button press
if (i > 0 && m_splitJoinState[i] == ESplitJoinState::AuthDone)
{
app.DebugPrintf("Splitscreen: slot %d auth done, joining immediately\n", i);
resetSplitJoinState(i);
ui.HidePressStart();
// Ensure the auth session is ready before joining.
// If still authenticating, defer to next frame instead of blocking.
{
auto slotState = MCAuthManager::Get().GetSlotState(i);
if (slotState == MCAuthManager::State::Authenticating ||
slotState == MCAuthManager::State::WaitingForCode)
{
app.DebugPrintf("Splitscreen: slot %d auth still in flight, deferring join\n", i);
setSplitAuthCompleted(i); // re-enter AuthDone state, retry next frame
continue;
}
}
if (level->isClientSide)
{
bool success = addLocalPlayer(i);
if (!success)
app.DebugPrintf("Splitscreen: addLocalPlayer(%d) failed\n", i);
}
else
{
shared_ptr<Player> pl = localplayers[i];
if (pl == nullptr)
createExtraLocalPlayer(i, (convStringToWstring(ProfileManager.GetGamertag(i))).c_str(), i, level->dimension->id);
}
continue;
}
bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0 && g_KBMInput.IsWindowFocused(); bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && xCurButtons != 0 && g_KBMInput.IsWindowFocused();
#else #else
bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && RenderManager.IsHiDef() && InputManager.ButtonPressed(i); bool tryJoin = !pause && !ui.IsIgnorePlayerJoinMenuDisplayed(ProfileManager.GetPrimaryPad()) && g_NetworkManager.SessionHasSpace() && RenderManager.IsHiDef() && InputManager.ButtonPressed(i);
@ -1695,17 +1751,35 @@ void Minecraft::run_middle()
#endif #endif
if( level->isClientSide ) if( level->isClientSide )
{ {
bool success=addLocalPlayer(i); #ifdef _WINDOWS64
// Splitscreen: state machine for secondary controller join.
if(!success) // Idle → open account picker → AuthUI
// AuthDone → Joining → addLocalPlayer → Idle
if (i > 0 && m_splitJoinState[i] == ESplitJoinState::Idle)
{ {
app.DebugPrintf("Bringing up the sign in ui\n"); // First press: open the account picker
ProfileManager.RequestSignInUI(false, g_NetworkManager.IsLocalGame(), true, false,true,&Minecraft::InGame_SignInReturned, this,i); static int s_splitscreenSlots[XUSER_MAX_COUNT] = {};
s_splitscreenSlots[i] = i;
app.DebugPrintf("Splitscreen: opening account picker for slot %d\n", i);
setSplitAuthOpened(i);
s_startPressLatch[i] = 0;
// Use Popup layer + Fullscreen group so auth UI renders above
// all other players' HUDs (avoids z-order issue with per-pad groups).
ui.NavigateToScene(i, eUIScene_MSAuth, &s_splitscreenSlots[i],
eUILayer_Popup, eUIGroup_Fullscreen);
} }
else else if (i == 0)
#endif
{ {
bool success=addLocalPlayer(i);
if(!success)
{
app.DebugPrintf("Bringing up the sign in ui\n");
ProfileManager.RequestSignInUI(false, g_NetworkManager.IsLocalGame(), true, false,true,&Minecraft::InGame_SignInReturned, this,i);
}
#ifdef __ORBIS__ #ifdef __ORBIS__
if(g_NetworkManager.IsLocalGame() == false) else if(!g_NetworkManager.IsLocalGame())
{ {
bool chatRestricted = false; bool chatRestricted = false;
ProfileManager.GetChatAndContentRestrictions(i,false,&chatRestricted,nullptr,nullptr); ProfileManager.GetChatAndContentRestrictions(i,false,&chatRestricted,nullptr,nullptr);
@ -2375,25 +2449,23 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures)
} }
#ifdef _WINDOWS64 #ifdef _WINDOWS64
// Mouse grab/release only for the primary (KBM) player — splitscreen // Mouse grab/release for the primary (KBM) player.
// players use controllers and must never fight over the cursor state. // Only ungrab when the PRIMARY player's own UI is blocking — other
// players' menus (splitscreen) should not steal KBM focus.
if (iPad == ProfileManager.GetPrimaryPad()) if (iPad == ProfileManager.GetPrimaryPad())
{ {
if ((screen != nullptr || ui.GetMenuDisplayed(iPad)) && g_KBMInput.IsMouseGrabbed()) int primaryPad = ProfileManager.GetPrimaryPad();
{ bool primaryUIBlocking = (screen != nullptr) || ui.GetMenuDisplayed(primaryPad);
if (primaryUIBlocking && g_KBMInput.IsMouseGrabbed())
g_KBMInput.SetMouseGrabbed(false); g_KBMInput.SetMouseGrabbed(false);
} else if (!primaryUIBlocking && !g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsWindowFocused())
g_KBMInput.SetMouseGrabbed(true);
} }
#endif #endif
if (screen == nullptr && !ui.GetMenuDisplayed(iPad) ) if (screen == nullptr && !ui.GetMenuDisplayed(iPad) )
{ {
#ifdef _WINDOWS64
if (iPad == ProfileManager.GetPrimaryPad() && !g_KBMInput.IsMouseGrabbed() && g_KBMInput.IsWindowFocused())
{
g_KBMInput.SetMouseGrabbed(true);
}
#endif
// 4J-PB - add some tooltips if required // 4J-PB - add some tooltips if required
int iA=-1, iB=-1, iX, iY=IDS_CONTROLS_INVENTORY, iLT=-1, iRT=-1, iLB=-1, iRB=-1, iLS=-1, iRS=-1; int iA=-1, iB=-1, iX, iY=IDS_CONTROLS_INVENTORY, iLT=-1, iRT=-1, iLB=-1, iRB=-1, iLS=-1, iRS=-1;
@ -4356,32 +4428,8 @@ void Minecraft::setLevel(MultiPlayerLevel *level, int message /*=-1*/, shared_pt
player = gameMode->createPlayer(level); player = gameMode->createPlayer(level);
PlayerUID playerXUIDOffline = INVALID_XUID; PlayerUID playerXUID = GameUUID::generateOffline(std::string(player->name.begin(), player->name.end()));
PlayerUID playerXUIDOnline = INVALID_XUID; player->setXuid(playerXUID);
ProfileManager.GetXUID(iPrimaryPlayer,&playerXUIDOffline,false);
ProfileManager.GetXUID(iPrimaryPlayer,&playerXUIDOnline,true);
#ifdef __PSVITA__
if(CGameNetworkManager::usingAdhocMode() && playerXUIDOnline.getOnlineID()[0] == 0)
{
// player doesn't have an online UID, set it from the player name
playerXUIDOnline.setForAdhoc();
}
#endif
#ifdef _WINDOWS64
// On Windows, the implementation has been changed to use a per-client pseudo XUID based on `uid.dat`.
// To maintain player data compatibility with existing worlds, the world host (the first player) will use the previous embedded pseudo XUID.
INetworkPlayer *localNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(iPrimaryPlayer);
if(localNetworkPlayer != nullptr && localNetworkPlayer->IsHost())
{
playerXUIDOffline = Win64Xuid::GetLegacyEmbeddedHostXuid();
}
else
{
playerXUIDOffline = Win64Xuid::ResolvePersistentXuid();
}
#endif
player->setXuid(playerXUIDOffline);
player->setOnlineXuid(playerXUIDOnline);
player->m_displayName = ProfileManager.GetDisplayName(iPrimaryPlayer); player->m_displayName = ProfileManager.GetDisplayName(iPrimaryPlayer);
@ -4454,6 +4502,7 @@ void Minecraft::setLevel(MultiPlayerLevel *level, int message /*=-1*/, shared_pt
if( m_pendingLocalConnections[i] != nullptr ) m_pendingLocalConnections[i]->close(); if( m_pendingLocalConnections[i] != nullptr ) m_pendingLocalConnections[i]->close();
m_pendingLocalConnections[i] = nullptr; m_pendingLocalConnections[i] = nullptr;
localplayers[i] = nullptr; localplayers[i] = nullptr;
delete localgameModes[i];
localgameModes[i] = nullptr; localgameModes[i] = nullptr;
} }
} }
@ -4558,24 +4607,11 @@ void Minecraft::respawnPlayer(int iPad, int dimension, int newEntityId)
EDefaultSkins skin = localPlayer->getPlayerDefaultSkin(); EDefaultSkins skin = localPlayer->getPlayerDefaultSkin();
player = localgameModes[iPad]->createPlayer(level); player = localgameModes[iPad]->createPlayer(level);
PlayerUID playerXUIDOffline = INVALID_XUID; // Carry UUID from old player, or generate offline UUID
PlayerUID playerXUIDOnline = INVALID_XUID; PlayerUID playerXUID = (localPlayer != nullptr && localPlayer->getXuid().isValid())
ProfileManager.GetXUID(iTempPad,&playerXUIDOffline,false); ? localPlayer->getXuid()
ProfileManager.GetXUID(iTempPad,&playerXUIDOnline,true); : GameUUID::generateOffline(std::string(player->name.begin(), player->name.end()));
#ifdef _WINDOWS64 player->setXuid(playerXUID);
// Same compatibility rule as create/init paths.
INetworkPlayer *localNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(iTempPad);
if(localNetworkPlayer != nullptr && localNetworkPlayer->IsHost())
{
playerXUIDOffline = Win64Xuid::GetLegacyEmbeddedHostXuid();
}
else
{
playerXUIDOffline = Win64Xuid::ResolvePersistentXuid();
}
#endif
player->setXuid(playerXUIDOffline);
player->setOnlineXuid(playerXUIDOnline);
player->setIsGuest( ProfileManager.IsGuest(iTempPad) ); player->setIsGuest( ProfileManager.IsGuest(iTempPad) );
player->m_displayName = ProfileManager.GetDisplayName(iPad); player->m_displayName = ProfileManager.GetDisplayName(iPad);
@ -4587,6 +4623,8 @@ void Minecraft::respawnPlayer(int iPad, int dimension, int newEntityId)
player->setCustomSkin(localPlayer->getCustomSkin()); player->setCustomSkin(localPlayer->getCustomSkin());
player->setPlayerDefaultSkin( skin ); player->setPlayerDefaultSkin( skin );
player->setCustomCape(localPlayer->getCustomCape()); player->setCustomCape(localPlayer->getCustomCape());
// Preserve Mojang skin texture key across respawn.
player->customTextureUrl = localPlayer->customTextureUrl;
player->m_sessionTimeStart = localPlayer->m_sessionTimeStart; player->m_sessionTimeStart = localPlayer->m_sessionTimeStart;
player->m_dimensionTimeStart = localPlayer->m_dimensionTimeStart; player->m_dimensionTimeStart = localPlayer->m_dimensionTimeStart;
player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, localPlayer->getAllPlayerGamePrivileges()); player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, localPlayer->getAllPlayerGamePrivileges());

View file

@ -110,6 +110,28 @@ public:
DisconnectPacket::eDisconnectReason m_connectionFailedReason[XUSER_MAX_COUNT]; DisconnectPacket::eDisconnectReason m_connectionFailedReason[XUSER_MAX_COUNT];
ClientConnection *m_pendingLocalConnections[XUSER_MAX_COUNT]; ClientConnection *m_pendingLocalConnections[XUSER_MAX_COUNT];
#ifdef _WINDOWS64
// Splitscreen join state machine per slot.
// Centralizes the auth UI lifecycle that was previously tracked
// via two independent booleans (m_splitscreenAuthPending/Done).
enum class ESplitJoinState : uint8_t {
Idle, // slot free, ready for Start press to initiate join
AuthUI, // account picker UI is open, block game loop join
AuthDone, // user picked an account, game loop should call addLocalPlayer
};
ESplitJoinState m_splitJoinState[XUSER_MAX_COUNT] = {};
void setSplitAuthOpened(int i) { m_splitJoinState[i] = ESplitJoinState::AuthUI; }
void setSplitAuthCompleted(int i){ m_splitJoinState[i] = ESplitJoinState::AuthDone; }
void setSplitAuthCancelled(int i){ m_splitJoinState[i] = ESplitJoinState::Idle; }
void resetSplitJoinState(int i) { m_splitJoinState[i] = ESplitJoinState::Idle; }
bool isAnySplitAuthUIOpen() const {
for (int i = 0; i < XUSER_MAX_COUNT; ++i)
if (m_splitJoinState[i] == ESplitJoinState::AuthUI) return true;
return false;
}
#endif
bool addLocalPlayer(int idx); // Re-arrange the screen and start the connection bool addLocalPlayer(int idx); // Re-arrange the screen and start the connection
void addPendingLocalConnection(int idx, ClientConnection *connection); void addPendingLocalConnection(int idx, ClientConnection *connection);
void connectionDisconnected(int idx, DisconnectPacket::eDisconnectReason reason) { m_connectionFailed[idx] = true; m_connectionFailedReason[idx] = reason; } void connectionDisconnected(int idx, DisconnectPacket::eDisconnectReason reason) { m_connectionFailed[idx] = true; m_connectionFailedReason[idx] = reason; }
@ -179,7 +201,7 @@ private:
int rightClickDelay; int rightClickDelay;
public: public:
// 4J- this should really be in localplayer // 4J- this should really be in localplayer
StatsCounter* stats[4]; StatsCounter* stats[XUSER_MAX_COUNT];
private: private:
wstring connectToIp; wstring connectToIp;

View file

@ -40,6 +40,12 @@
#endif #endif
#include "..\Minecraft.World\ConsoleSaveFileOriginal.h" #include "..\Minecraft.World\ConsoleSaveFileOriginal.h"
#include "..\Minecraft.World\Socket.h" #include "..\Minecraft.World\Socket.h"
#ifdef _WINDOWS64
#include "..\MCAuth\include\MCAuthManager.h"
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogger.h"
#endif
#include "..\Minecraft.World\net.minecraft.world.entity.h" #include "..\Minecraft.World\net.minecraft.world.entity.h"
#include "ProgressRenderer.h" #include "ProgressRenderer.h"
#include "ServerPlayer.h" #include "ServerPlayer.h"
@ -644,7 +650,48 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW
// 4J - Unused // 4J - Unused
//localIp = settings->getString(L"server-ip", L""); //localIp = settings->getString(L"server-ip", L"");
//onlineMode = settings->getBoolean(L"online-mode", true); if (ShouldUseDedicatedServerProperties())
{
onlineMode = GetDedicatedServerBool(settings, L"online-mode", true);
{
wstring wProvider = GetDedicatedServerString(settings, L"auth-provider", L"mojang");
authProvider = std::string(wProvider.begin(), wProvider.end());
if (authProvider != "mojang" && authProvider != "elyby")
authProvider = "mojang";
}
app.DebugPrintf("[Auth] Dedicated server: onlineMode=%d, authProvider=%s (from server.properties)\n",
(int)onlineMode, authProvider.c_str());
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::LogInfof("auth", "Dedicated server: onlineMode=%d, authProvider=%s (from server.properties)",
(int)onlineMode, authProvider.c_str());
#endif
}
else
{
#ifdef _WINDOWS64
// Embedded server (singleplayer/LAN): derive from whether we have an online account
auto& mgr = MCAuthManager::Get();
bool isLoggedIn = mgr.IsJavaLoggedIn();
bool hasToken = !mgr.GetJavaSession().accessToken.empty();
onlineMode = isLoggedIn && hasToken;
// Derive auth provider from active account
{
auto accounts = mgr.GetJavaAccounts();
int acctIdx = mgr.GetSlot(0).accountIndex.load();
if (acctIdx >= 0 && acctIdx < (int)accounts.size())
authProvider = accounts[acctIdx].authProvider;
else
authProvider = "mojang";
}
app.DebugPrintf("[Auth] Embedded server: IsJavaLoggedIn=%d, hasAccessToken=%d -> onlineMode=%d, authProvider=%s\n",
(int)isLoggedIn, (int)hasToken, (int)onlineMode, authProvider.c_str());
#else
// Console embedded servers: auth handled by platform (PSN/Xbox Live), not MCAuth
onlineMode = false;
authProvider = "mojang";
app.DebugPrintf("[Auth] Embedded server (console): onlineMode=false\n");
#endif
}
//motd = settings->getString(L"motd", L"A Minecraft Server"); //motd = settings->getString(L"motd", L"A Minecraft Server");
//motd.replace('<27>', '$'); //motd.replace('<27>', '$');

View file

@ -113,6 +113,7 @@ private:
CRITICAL_SECTION m_consoleInputCS; CRITICAL_SECTION m_consoleInputCS;
public: public:
bool onlineMode; bool onlineMode;
std::string authProvider = "mojang"; // "mojang" or "elyby"
bool animals; bool animals;
bool npcs; bool npcs;
bool pvp; bool pvp;

View file

@ -19,7 +19,7 @@ public:
WCHAR wNameXUID[32] = L""; WCHAR wNameXUID[32] = L"";
WCHAR wNameSkin[32] = L""; WCHAR wNameSkin[32] = L"";
WCHAR wNameCloak[32] = L""; WCHAR wNameCloak[32] = L"";
PlayerUID xuid=0LL; PlayerUID xuid = INVALID_XUID;
if (NameLen >31) if (NameLen >31)
@ -47,7 +47,11 @@ public:
{ {
ZeroMemory(wTemp,sizeof(WCHAR)*35); ZeroMemory(wTemp,sizeof(WCHAR)*35);
wcsncpy_s( wTemp, pAttributes[i].strValue, pAttributes[i].ValueLen); wcsncpy_s( wTemp, pAttributes[i].strValue, pAttributes[i].ValueLen);
xuid=_wcstoui64(wTemp,NULL,10); {
char narrow[64] = {};
wcstombs_s(nullptr, narrow, wTemp, _TRUNCATE);
xuid = PlayerUID::fromDashed(std::string(narrow));
}
} }
} }
else if (_wcsicmp(wAttName,L"cape")==0) else if (_wcsicmp(wAttName,L"cape")==0)
@ -68,7 +72,7 @@ public:
} }
// if the xuid hasn't been defined, then we can't use the data // if the xuid hasn't been defined, then we can't use the data
if(xuid!=0LL) if(xuid.isValid())
{ {
return CConsoleMinecraftApp::RegisterMojangData(wNameXUID , xuid, wNameSkin, wNameCloak ); return CConsoleMinecraftApp::RegisterMojangData(wNameXUID , xuid, wNameSkin, wNameCloak );
} }

View file

@ -8,14 +8,21 @@
#include "PlayerList.h" #include "PlayerList.h"
#include "MinecraftServer.h" #include "MinecraftServer.h"
#include "..\Minecraft.World\net.minecraft.network.h" #include "..\Minecraft.World\net.minecraft.network.h"
#include "..\Minecraft.World\net.minecraft.network.packet.h"
#include "..\Minecraft.World\pos.h" #include "..\Minecraft.World\pos.h"
#include "..\Minecraft.World\net.minecraft.world.level.dimension.h" #include "..\Minecraft.World\net.minecraft.world.level.dimension.h"
#include "..\Minecraft.World\net.minecraft.world.level.storage.h" #include "..\Minecraft.World\net.minecraft.world.level.storage.h"
#include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\net.minecraft.world.item.h"
#include "..\Minecraft.World\SharedConstants.h" #include "..\Minecraft.World\SharedConstants.h"
#include "..\Minecraft.World\GameUUID.h"
#include "Settings.h" #include "Settings.h"
#ifdef _WINDOWS64
#include "..\..\MCAuth\include\MCAuth.h"
#include <thread>
#endif
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
#include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\ServerLogManager.h"
#include "..\Minecraft.Server\ServerLogger.h"
#include "..\Minecraft.Server\Access\Access.h" #include "..\Minecraft.Server\Access\Access.h"
#include "..\Minecraft.World\Socket.h" #include "..\Minecraft.World\Socket.h"
#endif #endif
@ -23,6 +30,25 @@
// #include "PS3\Network\NetworkPlayerSony.h" // #include "PS3\Network\NetworkPlayerSony.h"
// #endif // #endif
// Auth logging: use INFO-level server logger on dedicated server, DebugPrintf on client
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
static void AuthLog(const char* fmt, ...)
{
char buf[2048];
va_list args;
va_start(args, fmt);
vsnprintf_s(buf, sizeof(buf), _TRUNCATE, fmt, args);
va_end(args);
// Strip trailing newline for server logger (it adds its own)
size_t len = strlen(buf);
while (len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) buf[--len] = '\0';
ServerRuntime::LogInfof("auth", "%s", buf);
}
#define AUTH_LOG(fmt, ...) AuthLog(fmt, ##__VA_ARGS__)
#else
#define AUTH_LOG(fmt, ...) app.DebugPrintf(fmt, ##__VA_ARGS__)
#endif
Random *PendingConnection::random = new Random(); Random *PendingConnection::random = new Random();
#ifdef _WINDOWS64 #ifdef _WINDOWS64
@ -47,6 +73,43 @@ namespace
} }
#endif #endif
struct UgcGatherResult
{
PlayerUID *xuids;
DWORD cappedCount;
BYTE friendsOnlyBits;
BYTE cappedHostIndex;
char uniqueMapName[14];
};
static UgcGatherResult GatherUgcData()
{
UgcGatherResult r = {};
StorageManager.GetSaveUniqueFilename(r.uniqueMapName);
r.xuids = new PlayerUID[MINECRAFT_NET_MAX_PLAYERS];
DWORD count = 0;
DWORD hostIndex = 0;
PlayerList *playerList = MinecraftServer::getInstance()->getPlayers();
for (auto& player : playerList->players)
{
if (player != nullptr && player->connection->m_xuid != INVALID_XUID)
{
if (player->connection->m_friendsOnlyUGC)
r.friendsOnlyBits |= (1 << count);
r.xuids[count] = player->connection->m_xuid;
if (player->connection->getNetworkPlayer() != nullptr && player->connection->getNetworkPlayer()->IsHost())
hostIndex = count;
++count;
}
}
r.cappedCount = (count > 255u) ? 255u : count;
r.cappedHostIndex = (hostIndex >= 255u) ? 254 : static_cast<BYTE>(hostIndex);
return r;
}
PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id) PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id)
{ {
// 4J - added initialisers // 4J - added initialisers
@ -63,6 +126,8 @@ PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, co
PendingConnection::~PendingConnection() PendingConnection::~PendingConnection()
{ {
if (m_authVerifyResult)
m_authVerifyResult->cancelled.store(true, std::memory_order_release);
delete connection; delete connection;
} }
@ -73,6 +138,51 @@ void PendingConnection::tick()
this->handleAcceptedLogin(acceptedLogin); this->handleAcceptedLogin(acceptedLogin);
acceptedLogin = nullptr; acceptedLogin = nullptr;
} }
#ifdef _WINDOWS64
// Poll async Mojang verification result
if (m_authState == eAuth_Verifying && m_authVerifyResult && m_authVerifyResult->ready.load(std::memory_order_acquire))
{
std::lock_guard<std::mutex> lock(m_authVerifyResult->mutex);
if (m_authVerifyResult->success)
{
m_authUsername = m_authVerifyResult->username;
m_authUuid = MCAuth::DashUuid(m_authVerifyResult->uuid);
AUTH_LOG("[Auth] %s verification SUCCESS for '%s' (uuid=%s)\n",
m_authScheme.c_str(), m_authUsername.c_str(), m_authUuid.c_str());
// Store skin data for later use in placeNewPlayer
m_authSkinData = std::move(m_authVerifyResult->skinData);
if (!m_authSkinData.empty())
{
if (m_authScheme == "elyby")
m_authSkinUrl = MCAuth::MakeElybySkinKey(m_authVerifyResult->uuid);
else
m_authSkinUrl = MCAuth::MakeSkinKey(m_authVerifyResult->uuid);
AUTH_LOG("[Auth] Downloaded %s skin for %s (%zu bytes)\n",
m_authScheme.c_str(), m_authUsername.c_str(), m_authSkinData.size());
}
wstring wUuid(m_authUuid.begin(), m_authUuid.end());
wstring wName(m_authUsername.begin(), m_authUsername.end());
// Send skin key + bytes inline so the client doesn't need a separate download
wstring wSkinKey(m_authSkinUrl.begin(), m_authSkinUrl.end());
connection->send(make_shared<AuthResultPacket>(true, wUuid, wName, L"", wSkinKey, m_authSkinData));
m_authState = eAuth_WaitingAck;
}
else
{
AUTH_LOG("[Auth] %s verification FAILED for pending connection (error=%s), disconnecting\n",
m_authScheme.c_str(), m_authVerifyResult->errorDetail.c_str());
connection->send(make_shared<AuthResultPacket>(false, L"", L"", L"Authentication failed"));
connection->sendAndQuit();
m_authState = eAuth_Done;
done = true;
}
}
#endif
if (_tick++ == MAX_TICKS_BEFORE_LOGIN) if (_tick++ == MAX_TICKS_BEFORE_LOGIN)
{ {
disconnect(DisconnectPacket::eDisconnect_LoginTooLong); disconnect(DisconnectPacket::eDisconnect_LoginTooLong);
@ -85,9 +195,23 @@ void PendingConnection::tick()
void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason) void PendingConnection::disconnect(DisconnectPacket::eDisconnectReason reason)
{ {
// try { // 4J - removed try/catch if (m_authVerifyResult)
// logger.info("Disconnecting " + getName() + ": " + reason); m_authVerifyResult->cancelled.store(true, std::memory_order_release);
app.DebugPrintf("Pending connection disconnect: %d\n", reason );
const char* reasonStr = "unknown";
switch (reason)
{
case DisconnectPacket::eDisconnect_Closed: reasonStr = "Closed"; break;
case DisconnectPacket::eDisconnect_Kicked: reasonStr = "Kicked"; break;
case DisconnectPacket::eDisconnect_LoginTooLong: reasonStr = "LoginTooLong"; break;
case DisconnectPacket::eDisconnect_OutdatedServer: reasonStr = "OutdatedServer"; break;
case DisconnectPacket::eDisconnect_OutdatedClient: reasonStr = "OutdatedClient"; break;
case DisconnectPacket::eDisconnect_ServerFull: reasonStr = "ServerFull"; break;
case DisconnectPacket::eDisconnect_AuthFailed: reasonStr = "AuthFailed"; break;
case DisconnectPacket::eDisconnect_UnexpectedPacket: reasonStr = "UnexpectedPacket"; break;
default: break;
}
AUTH_LOG("[Auth] Pending connection disconnect: reason=%d (%s)\n", reason, reasonStr);
connection->send(std::make_shared<DisconnectPacket>(reason)); connection->send(std::make_shared<DisconnectPacket>(reason));
connection->sendAndQuit(); connection->sendAndQuit();
done = true; done = true;
@ -119,56 +243,72 @@ void PendingConnection::handlePreLogin(shared_ptr<PreLoginPacket> packet)
void PendingConnection::sendPreLoginResponse() void PendingConnection::sendPreLoginResponse()
{ {
// 4J Stu - Calculate the players with UGC privileges set // 4J Stu - Calculate the players with UGC privileges set
PlayerUID *ugcXuids = new PlayerUID[MINECRAFT_NET_MAX_PLAYERS]; UgcGatherResult ugc = GatherUgcData();
DWORD ugcXuidCount = 0; connection->send(std::make_shared<PreLoginPacket>(L"-", ugc.xuids, ugc.cappedCount, ugc.friendsOnlyBits, server->m_ugcPlayersVersion, ugc.uniqueMapName, app.GetGameHostOption(eGameHostOption_All), ugc.cappedHostIndex, server->m_texturePackId));
DWORD hostIndex = 0;
BYTE ugcFriendsOnlyBits = 0;
char szUniqueMapName[14];
StorageManager.GetSaveUniqueFilename(szUniqueMapName); // --- Auth handshake ---
// Generate random serverId (20 hex chars)
PlayerList *playerList = MinecraftServer::getInstance()->getPlayers();
for(auto& player : playerList->players)
{ {
// If the offline Xuid is invalid but the online one is not then that's guest which we should ignore static const char hex[] = "0123456789abcdef";
// If the online Xuid is invalid but the offline one is not then we are definitely an offline game so dont care about UGC m_serverId.resize(20);
for (int i = 0; i < 20; i++)
m_serverId[i] = hex[random->nextInt(16)];
}
// PADDY - this is failing when a local player with chat restrictions joins an online game vector<wstring> schemes;
if (server->onlineMode)
if( player != nullptr && player->connection->m_offlineXUID != INVALID_XUID && player->connection->m_onlineXUID != INVALID_XUID ) {
if (server->authProvider == "elyby")
{ {
if( player->connection->m_friendsOnlyUGC ) schemes.push_back(L"elyby");
{ AUTH_LOG("[Auth] Server onlineMode=true, authProvider=elyby, offering scheme: [elyby]\n");
ugcFriendsOnlyBits |= (1<<ugcXuidCount); }
} else
// Need to use the online XUID otherwise friend checks will fail on the client {
ugcXuids[ugcXuidCount] = player->connection->m_onlineXUID; schemes.push_back(L"mojang");
AUTH_LOG("[Auth] Server onlineMode=true, authProvider=mojang, offering scheme: [mojang]\n");
if( player->connection->getNetworkPlayer() != nullptr && player->connection->getNetworkPlayer()->IsHost() ) hostIndex = ugcXuidCount;
++ugcXuidCount;
} }
} }
#if 0
if (false)// server->onlineMode) // 4J - removed
{
loginKey = L"TOIMPLEMENT"; // 4J - todo Long.toHexString(random.nextLong());
connection->send( shared_ptr<PreLoginPacket>( new PreLoginPacket(loginKey, ugcXuids, ugcXuidCount, ugcFriendsOnlyBits, server->m_ugcPlayersVersion, szUniqueMapName,app.GetGameHostOption(eGameHostOption_All),hostIndex) ) );
}
else else
#endif
{ {
DWORD cappedCount = (ugcXuidCount > 255u) ? 255u : ugcXuidCount; schemes.push_back(L"mojang");
BYTE cappedHostIndex = (hostIndex >= 255u) ? 254 : static_cast<BYTE>(hostIndex); schemes.push_back(L"offline");
connection->send(std::make_shared<PreLoginPacket>(L"-", ugcXuids, cappedCount, ugcFriendsOnlyBits, server->m_ugcPlayersVersion, szUniqueMapName, app.GetGameHostOption(eGameHostOption_All), cappedHostIndex, server->m_texturePackId)); AUTH_LOG("[Auth] Server onlineMode=false, offering schemes: [mojang, offline]\n");
} }
wstring wServerId(m_serverId.begin(), m_serverId.end());
connection->send(make_shared<AuthSchemePacket>(schemes, wServerId));
m_authState = eAuth_WaitingResponse;
AUTH_LOG("[Auth] Sent AuthSchemePacket to client, serverId=%s\n", m_serverId.c_str());
} }
void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet) void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
{ {
// printf("Server: handleLogin\n"); // printf("Server: handleLogin\n");
//name = packet->userName; //name = packet->userName;
// Reject login if auth handshake has not completed.
// eAuth_None means no auth handshake occurred — reject to prevent auth bypass.
if (m_authState == eAuth_WaitingAck)
{
// LoginPacket completes the auth handshake
wstring wName(m_authUsername.begin(), m_authUsername.end());
name = wName;
m_authState = eAuth_Done;
AUTH_LOG("[Auth] Auth handshake complete for '%s' (implicit ack via LoginPacket)\n", m_authUsername.c_str());
}
else if (m_authState == eAuth_Done)
{
// Already completed — allow through
}
else
{
// eAuth_None, eAuth_WaitingResponse, eAuth_Verifying — all invalid
AUTH_LOG("[Auth] Received LoginPacket before auth completed (state=%d), disconnecting\n", (int)m_authState);
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
if (packet->clientVersion != SharedConstants::NETWORK_PROTOCOL_VERSION) if (packet->clientVersion != SharedConstants::NETWORK_PROTOCOL_VERSION)
{ {
app.DebugPrintf("Client version is %d not equal to %d\n", packet->clientVersion, SharedConstants::NETWORK_PROTOCOL_VERSION); app.DebugPrintf("Client version is %d not equal to %d\n", packet->clientVersion, SharedConstants::NETWORK_PROTOCOL_VERSION);
@ -184,56 +324,43 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
} }
//if (true)// 4J removed !server->onlineMode) //if (true)// 4J removed !server->onlineMode)
bool sentDisconnect = false; PlayerUID loginXuid = packet->m_xuid;
// INVALID_XUID may indicate a truncated packet (readPlayerUID returns {0,0} on EOF)
// Use the same Xuid choice as handleAcceptedLogin (offline first, online fallback). if (loginXuid == INVALID_XUID)
//
PlayerUID loginXuid = packet->m_offlineXuid;
if (loginXuid == INVALID_XUID) loginXuid = packet->m_onlineXuid;
bool duplicateXuid = false;
if (loginXuid != INVALID_XUID && server->getPlayers()->getPlayer(loginXuid) != nullptr)
{ {
duplicateXuid = true; AUTH_LOG("[Auth] Warning: received INVALID_XUID in LoginPacket — possible truncated packet or offline client\n");
}
else if (packet->m_onlineXuid != INVALID_XUID &&
packet->m_onlineXuid != loginXuid &&
server->getPlayers()->getPlayer(packet->m_onlineXuid) != nullptr)
{
duplicateXuid = true;
} }
bool bannedXuid = false; bool duplicateXuid = (loginXuid != INVALID_XUID && server->getPlayers()->getPlayer(loginXuid) != nullptr);
if (loginXuid != INVALID_XUID)
bool bannedXuid = (loginXuid != INVALID_XUID && server->getPlayers()->isXuidBanned(loginXuid));
// Also check ban against server-verified auth UUID to prevent bypass via INVALID_XUID
if (!bannedXuid && !m_authUuid.empty())
{ {
bannedXuid = server->getPlayers()->isXuidBanned(loginXuid); GameUUID authUid = GameUUID::fromDashed(m_authUuid);
} if (authUid.isValid())
if (!bannedXuid && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid) {
{ bannedXuid = server->getPlayers()->isXuidBanned(authUid);
bannedXuid = server->getPlayers()->isXuidBanned(packet->m_onlineXuid); }
} }
bool whitelistSatisfied = true; bool whitelistSatisfied = true;
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
if (ServerRuntime::Access::IsWhitelistEnabled()) if (ServerRuntime::Access::IsWhitelistEnabled())
{ {
whitelistSatisfied = false; whitelistSatisfied = (loginXuid != INVALID_XUID && ServerRuntime::Access::IsPlayerWhitelisted(loginXuid));
if (loginXuid != INVALID_XUID) if (!whitelistSatisfied && !m_authUuid.empty())
{ {
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(loginXuid); GameUUID authUid = GameUUID::fromDashed(m_authUuid);
} if (authUid.isValid())
if (!whitelistSatisfied && packet->m_onlineXuid != INVALID_XUID && packet->m_onlineXuid != loginXuid) {
{ whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(authUid);
whitelistSatisfied = ServerRuntime::Access::IsPlayerWhitelisted(packet->m_onlineXuid); }
} }
} }
#endif #endif
if( sentDisconnect ) if (bannedXuid)
{
// Do nothing
}
else if (bannedXuid)
{ {
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_BannedXuid); ServerRuntime::ServerLogManager::OnRejectedPlayerLogin(GetPendingConnectionSmallId(connection), name, ServerRuntime::ServerLogManager::eLoginRejectReason_BannedXuid);
@ -289,31 +416,6 @@ void PendingConnection::handleLogin(shared_ptr<LoginPacket> packet)
{ {
handleAcceptedLogin(packet); handleAcceptedLogin(packet);
} }
//else
{
//4J - removed
#if 0
new Thread() {
public void run() {
try {
String key = loginKey;
URL url = new URL("http://www.minecraft.net/game/checkserver.jsp?user=" + URLEncoder.encode(packet.userName, "UTF-8") + "&serverId=" + URLEncoder.encode(key, "UTF-8"));
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
String msg = br.readLine();
br.close();
if (msg.equals("YES")) {
acceptedLogin = packet;
} else {
disconnect("Failed to verify username!");
}
} catch (Exception e) {
disconnect("Failed to verify username! [internal error " + e + "]");
e.printStackTrace();
}
}
}.start();
#endif
}
} }
@ -321,28 +423,294 @@ void PendingConnection::handleAcceptedLogin(shared_ptr<LoginPacket> packet)
{ {
if(packet->m_ugcPlayersVersion != server->m_ugcPlayersVersion) if(packet->m_ugcPlayersVersion != server->m_ugcPlayersVersion)
{ {
// Send the pre-login packet again with the new list of players // UGC version mismatch — resend pre-login info but do NOT restart auth handshake.
sendPreLoginResponse(); // Only resend the PreLoginPacket with updated UGC player list; the auth state
// (m_authState, m_authUsername, m_authUuid) remains valid from the completed handshake.
UgcGatherResult ugc = GatherUgcData();
connection->send(std::make_shared<PreLoginPacket>(L"-", ugc.xuids, ugc.cappedCount, ugc.friendsOnlyBits, server->m_ugcPlayersVersion, ugc.uniqueMapName, app.GetGameHostOption(eGameHostOption_All), ugc.cappedHostIndex, server->m_texturePackId));
return; return;
} }
// Guests use the online xuid, everyone else uses the offline one PlayerUID playerXuid = packet->m_xuid;
PlayerUID playerXuid = packet->m_offlineXuid; // Fallback: generate offline UUID from player name if still invalid
if(playerXuid == INVALID_XUID) playerXuid = packet->m_onlineXuid; if (playerXuid == INVALID_XUID)
{
if (name.empty())
{
AUTH_LOG("[Auth] Rejecting login with empty name and no UUID\n");
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
playerXuid = GameUUID::generateOffline(std::string(name.begin(), name.end()));
}
shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid,packet->m_onlineXuid); shared_ptr<ServerPlayer> playerEntity = server->getPlayers()->getPlayerForLogin(this, name, playerXuid);
if (playerEntity != nullptr) if (playerEntity != nullptr)
{ {
// Use server-verified auth UUID instead of client-supplied packet->m_mojangUuid
if (!m_authUuid.empty())
{
GameUUID verifiedUuid = GameUUID::fromDashed(m_authUuid);
if (verifiedUuid.isValid())
{
playerEntity->setXuid(verifiedUuid);
}
}
#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)
ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name); ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name);
#endif #endif
server->getPlayers()->placeNewPlayer(connection, playerEntity, packet); server->getPlayers()->placeNewPlayer(connection, playerEntity, packet);
// Override with Mojang skin after placeNewPlayer has set the DLC skin
if (!m_authSkinData.empty() && !m_authSkinUrl.empty())
{
wstring wSkinKey(m_authSkinUrl.begin(), m_authSkinUrl.end());
// AddMemoryTextureFile takes ownership of the pointer — must be heap-allocated
DWORD skinSize = (DWORD)m_authSkinData.size();
PBYTE skinBuf = new BYTE[skinSize];
memcpy(skinBuf, m_authSkinData.data(), skinSize);
app.AddMemoryTextureFile(wSkinKey, skinBuf, skinSize);
playerEntity->customTextureUrl = wSkinKey;
app.DebugPrintf("Set Mojang skin for player %s: %s (%zu bytes)\n",
m_authUsername.c_str(), m_authSkinUrl.c_str(), m_authSkinData.size());
// Send Mojang skin to all other clients
for (auto& otherPlayer : server->getPlayers()->players)
{
if (otherPlayer != nullptr && otherPlayer != playerEntity && otherPlayer->connection != nullptr)
{
// Send the skin PNG data so the other client has it in memory
PBYTE otherBuf = new BYTE[skinSize];
memcpy(otherBuf, m_authSkinData.data(), skinSize);
otherPlayer->connection->send(
std::make_shared<TextureAndGeometryPacket>(wSkinKey, otherBuf, skinSize));
// Notify the other client that this player now uses this skin
otherPlayer->connection->send(
std::make_shared<TextureAndGeometryChangePacket>(playerEntity, wSkinKey));
}
}
// Send existing Mojang skins to the new joiner
for (auto& otherPlayer : server->getPlayers()->players)
{
if (otherPlayer != nullptr && otherPlayer != playerEntity &&
!otherPlayer->customTextureUrl.empty() &&
(otherPlayer->customTextureUrl.substr(0, 6) == L"mojang" ||
otherPlayer->customTextureUrl.substr(0, 5) == L"elyby"))
{
PBYTE existingData = nullptr;
DWORD existingSize = 0;
app.GetMemFileDetails(otherPlayer->customTextureUrl, &existingData, &existingSize);
if (existingData != nullptr && existingSize > 0)
{
PBYTE copyBuf = new BYTE[existingSize];
memcpy(copyBuf, existingData, existingSize);
playerEntity->connection->send(
std::make_shared<TextureAndGeometryPacket>(otherPlayer->customTextureUrl, copyBuf, existingSize));
playerEntity->connection->send(
std::make_shared<TextureAndGeometryChangePacket>(otherPlayer, otherPlayer->customTextureUrl));
}
}
}
}
connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor connection = nullptr; // We've moved responsibility for this over to the new PlayerConnection, nullptr so we don't delete our reference to it here in our dtor
} }
else
{
disconnect(DisconnectPacket::eDisconnect_ServerFull);
return;
}
done = true; done = true;
} }
void PendingConnection::handleAuthResponse(shared_ptr<AuthResponsePacket> packet)
{
if (m_authState != eAuth_WaitingResponse)
{
AUTH_LOG("[Auth] Received AuthResponse in unexpected state %d, disconnecting\n", (int)m_authState);
disconnect(DisconnectPacket::eDisconnect_UnexpectedPacket);
return;
}
// Convert wstring fields to std::string for MCAuth
string scheme(packet->chosenScheme.begin(), packet->chosenScheme.end());
string username(packet->username.begin(), packet->username.end());
string uuid(packet->mojangUuid.begin(), packet->mojangUuid.end());
AUTH_LOG("[Auth] Received AuthResponse: scheme='%s', username='%s', uuid='%s'\n",
scheme.c_str(), username.c_str(), uuid.c_str());
if (username.empty())
{
AUTH_LOG("[Auth] REJECTED: empty username\n");
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
return;
}
m_authScheme = scheme;
#ifdef _WINDOWS64
if (scheme == "mojang")
{
AUTH_LOG("[Auth] Starting Mojang session verification for '%s'...\n", username.c_str());
// Verify with Mojang sessionserver in a background thread
// shared_ptr so the result outlives PendingConnection if destroyed
m_authState = eAuth_Verifying;
m_authVerifyResult = std::make_shared<AuthVerifyResult>();
string serverId = m_serverId;
auto sharedResult = m_authVerifyResult; // capture shared_ptr by value
std::thread([username, serverId, sharedResult]() {
try {
if (sharedResult->cancelled.load(std::memory_order_acquire))
return;
string error;
auto result = MCAuth::HasJoined(username, serverId, error);
// Client may have disconnected
if (sharedResult->cancelled.load(std::memory_order_acquire))
return;
std::vector<uint8_t> skinBytes;
if (result.success && !result.skinUrl.empty())
{
string skinError;
skinBytes = MCAuth::FetchSkinPng(result.skinUrl, skinError);
}
if (sharedResult->cancelled.load(std::memory_order_acquire))
return;
// Prevents data race with tick()
{
std::lock_guard<std::mutex> lock(sharedResult->mutex);
sharedResult->success = result.success;
if (result.success)
{
sharedResult->username = result.username;
sharedResult->uuid = result.uuid;
sharedResult->skinUrl = result.skinUrl;
sharedResult->skinData = std::move(skinBytes);
}
else
{
sharedResult->errorDetail = error;
}
}
// Signal main thread
sharedResult->ready.store(true, std::memory_order_release);
}
catch (...) {
std::lock_guard<std::mutex> lock(sharedResult->mutex);
sharedResult->success = false;
sharedResult->errorDetail = "unknown exception";
sharedResult->ready.store(true, std::memory_order_release);
}
}).detach();
}
else if (scheme == "elyby")
{
AUTH_LOG("[Auth] Starting ely.by session verification for '%s'...\n", username.c_str());
m_authState = eAuth_Verifying;
m_authVerifyResult = std::make_shared<AuthVerifyResult>();
string capturedServerId = m_serverId;
auto sharedResult = m_authVerifyResult;
std::thread([username, capturedServerId, sharedResult]() {
try {
if (sharedResult->cancelled.load(std::memory_order_acquire))
return;
string error;
auto result = MCAuth::ElybyHasJoined(username, capturedServerId, error);
if (sharedResult->cancelled.load(std::memory_order_acquire))
return;
std::vector<uint8_t> skinBytes;
if (result.success && !result.skinUrl.empty())
{
string skinError;
skinBytes = MCAuth::FetchSkinPng(result.skinUrl, skinError);
}
if (sharedResult->cancelled.load(std::memory_order_acquire))
return;
{
std::lock_guard<std::mutex> lock(sharedResult->mutex);
sharedResult->success = result.success;
if (result.success)
{
sharedResult->username = result.username;
sharedResult->uuid = result.uuid;
sharedResult->skinUrl = result.skinUrl;
sharedResult->skinData = std::move(skinBytes);
}
else
{
sharedResult->errorDetail = error;
}
}
sharedResult->ready.store(true, std::memory_order_release);
}
catch (const std::exception& ex) {
std::lock_guard<std::mutex> lock(sharedResult->mutex);
sharedResult->success = false;
sharedResult->errorDetail = std::string("exception: ") + ex.what();
sharedResult->ready.store(true, std::memory_order_release);
}
catch (...) {
std::lock_guard<std::mutex> lock(sharedResult->mutex);
sharedResult->success = false;
sharedResult->errorDetail = "unknown exception";
sharedResult->ready.store(true, std::memory_order_release);
}
}).detach();
}
else
#endif
if (scheme == "offline")
{
if (server->onlineMode)
{
AUTH_LOG("[Auth] REJECTED: Client chose 'offline' but server requires online-mode (onlineMode=true)\n");
connection->send(make_shared<AuthResultPacket>(
false, L"", L"", L"Server requires Microsoft authentication"));
connection->sendAndQuit();
m_authState = eAuth_Done;
done = true;
return;
}
// Server-generated UUID prevents UUID forgery
GameUUID offlineGuid = GameUUID::generateOffline(username);
std::string offlineUuid = offlineGuid.toDashed();
AUTH_LOG("[Auth] Accepted offline auth for '%s' (server-assigned uuid=%s, client-sent uuid=%s)\n",
username.c_str(), offlineUuid.c_str(), uuid.c_str());
m_authUsername = username;
m_authUuid = offlineUuid;
wstring wUuid(offlineUuid.begin(), offlineUuid.end());
wstring wName(username.begin(), username.end());
connection->send(make_shared<AuthResultPacket>(true, wUuid, wName, L""));
m_authState = eAuth_WaitingAck;
}
else
{
AUTH_LOG("[Auth] REJECTED: Unknown auth scheme '%s', disconnecting\n", scheme.c_str());
disconnect(DisconnectPacket::eDisconnect_AuthFailed);
}
}
void PendingConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects) void PendingConnection::onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects)
{ {
// logger.info(getName() + " lost connection"); // logger.info(getName() + " lost connection");

View file

@ -1,10 +1,15 @@
#pragma once #pragma once
#include "..\Minecraft.World\PacketListener.h" #include "..\Minecraft.World\PacketListener.h"
#include <string>
#include <atomic>
#include <mutex>
#include <vector>
class MinecraftServer; class MinecraftServer;
class Socket; class Socket;
class LoginPacket; class LoginPacket;
class Connection; class Connection;
class Random; class Random;
class AuthResponsePacket;
using namespace std; using namespace std;
class PendingConnection : public PacketListener class PendingConnection : public PacketListener
@ -27,6 +32,32 @@ private:
shared_ptr<LoginPacket> acceptedLogin; shared_ptr<LoginPacket> acceptedLogin;
wstring loginKey; wstring loginKey;
// Auth handshake state
enum eAuthState { eAuth_None, eAuth_WaitingResponse, eAuth_Verifying, eAuth_WaitingAck, eAuth_Done };
eAuthState m_authState = eAuth_None;
std::string m_serverId; // random hex challenge
std::string m_authUsername; // username from auth response
std::string m_authUuid; // uuid from auth response (dashed)
std::string m_authScheme; // "mojang" or "offline"
// Thread-safe verification result (heap-allocated so detached thread cannot UAF)
struct AuthVerifyResult {
std::atomic<bool> ready{false};
std::atomic<bool> cancelled{false};
std::mutex mutex; // protects fields below
bool success = false;
std::string username;
std::string uuid; // undashed from HasJoined
std::string skinUrl; // Mojang skin texture URL
std::vector<uint8_t> skinData; // downloaded skin PNG bytes
std::string errorDetail; // diagnostic info on failure
};
std::shared_ptr<AuthVerifyResult> m_authVerifyResult;
// Skin data received from Mojang after auth verification
std::string m_authSkinUrl; // texture key for this player's skin
std::vector<uint8_t> m_authSkinData; // raw PNG bytes
public: public:
PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id); PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id);
~PendingConnection(); ~PendingConnection();
@ -35,6 +66,7 @@ public:
virtual void handlePreLogin(shared_ptr<PreLoginPacket> packet); virtual void handlePreLogin(shared_ptr<PreLoginPacket> packet);
virtual void handleLogin(shared_ptr<LoginPacket> packet); virtual void handleLogin(shared_ptr<LoginPacket> packet);
virtual void handleAcceptedLogin(shared_ptr<LoginPacket> packet); virtual void handleAcceptedLogin(shared_ptr<LoginPacket> packet);
virtual void handleAuthResponse(shared_ptr<AuthResponsePacket> packet);
virtual void onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects); virtual void onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects);
virtual void handleGetInfo(shared_ptr<GetInfoPacket> packet); virtual void handleGetInfo(shared_ptr<GetInfoPacket> packet);
virtual void handleKeepAlive(shared_ptr<KeepAlivePacket> packet); virtual void handleKeepAlive(shared_ptr<KeepAlivePacket> packet);

View file

@ -80,8 +80,7 @@ PlayerConnection::PlayerConnection(MinecraftServer *server, Connection *connecti
m_bWasKicked = false; m_bWasKicked = false;
m_friendsOnlyUGC = false; m_friendsOnlyUGC = false;
m_offlineXUID = INVALID_XUID; m_xuid = INVALID_XUID;
m_onlineXUID = INVALID_XUID;
m_bHasClientTickedOnce = false; m_bHasClientTickedOnce = false;
m_logSmallId = 0; m_logSmallId = 0;
@ -1631,7 +1630,12 @@ bool PlayerConnection::isDisconnected()
void PlayerConnection::handleDebugOptions(shared_ptr<DebugOptionsPacket> packet) void PlayerConnection::handleDebugOptions(shared_ptr<DebugOptionsPacket> packet)
{ {
//Player player = dynamic_pointer_cast<Player>( player->shared_from_this() ); // Only allow debug options if the player is an operator (host or moderator with cheats enabled).
// Without this check, any client could send a DebugOptionsPacket to any server and
// grant themselves debug privileges like "Craft Anything" - even on servers they don't own.
if (!server->getPlayers()->isOp(player))
return;
player->SetDebugOptions(packet->m_uiVal); player->SetDebugOptions(packet->m_uiVal);
} }

View file

@ -18,8 +18,7 @@ public:
bool done; bool done;
CRITICAL_SECTION done_cs; CRITICAL_SECTION done_cs;
// 4J Stu - Added this so that we can manage UGC privileges PlayerUID m_xuid;
PlayerUID m_offlineXUID, m_onlineXUID;
bool m_friendsOnlyUGC; bool m_friendsOnlyUGC;
private: private:

View file

@ -18,7 +18,8 @@
#include "..\Minecraft.World\ArrayWithLength.h" #include "..\Minecraft.World\ArrayWithLength.h"
#include "..\Minecraft.World\net.minecraft.network.packet.h" #include "..\Minecraft.World\net.minecraft.network.packet.h"
#include "..\Minecraft.World\net.minecraft.network.h" #include "..\Minecraft.World\net.minecraft.network.h"
#include "Windows64\Windows64_Xuid.h" #include "..\Minecraft.World\GameUUID.h"
#include "Windows64\Windows64_Uuid.h"
#ifdef _WINDOWS64 #ifdef _WINDOWS64
#include "Windows64\Network\WinsockNetLayer.h" #include "Windows64\Network\WinsockNetLayer.h"
#endif #endif
@ -90,6 +91,9 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
bool newPlayer = playerTag == nullptr; bool newPlayer = playerTag == nullptr;
// UUID is now set by PendingConnection::handleAcceptedLogin using the
// server-verified m_authUuid — do NOT trust packet->m_mojangUuid (client-supplied).
player->setLevel(server->getLevel(player->dimension)); player->setLevel(server->getLevel(player->dimension));
player->gameMode->setLevel(static_cast<ServerLevel *>(player->level)); player->gameMode->setLevel(static_cast<ServerLevel *>(player->level));
@ -107,7 +111,7 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
{ {
if( networkPlayer != nullptr ) if( networkPlayer != nullptr )
{ {
((NetworkPlayerSony *)networkPlayer)->SetUID( packet->m_onlineXuid ); ((NetworkPlayerSony *)networkPlayer)->SetUID( packet->m_xuid );
} }
} }
#endif #endif
@ -213,7 +217,7 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
wprintf(L"Sending texture packet to get custom skin %ls from player %ls\n",player->customTextureUrl2.c_str(), player->name.c_str()); wprintf(L"Sending texture packet to get custom skin %ls from player %ls\n",player->customTextureUrl2.c_str(), player->name.c_str());
#endif #endif
playerConnection->send(std::make_shared<TexturePacket>( playerConnection->send(std::make_shared<TexturePacket>(
player->customTextureUrl, player->customTextureUrl2,
nullptr, nullptr,
static_cast<DWORD>(0) static_cast<DWORD>(0)
)); ));
@ -244,8 +248,7 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr<ServerPlayer>
// 4J Added to store UGC settings // 4J Added to store UGC settings
playerConnection->m_friendsOnlyUGC = packet->m_friendsOnlyUGC; playerConnection->m_friendsOnlyUGC = packet->m_friendsOnlyUGC;
playerConnection->m_offlineXUID = packet->m_offlineXuid; playerConnection->m_xuid = packet->m_xuid;
playerConnection->m_onlineXUID = packet->m_onlineXuid;
// This player is now added to the list, so incrementing this value invalidates all previous PreLogin packets // This player is now added to the list, so incrementing this value invalidates all previous PreLogin packets
if(packet->m_friendsOnlyUGC) ++server->m_ugcPlayersVersion; if(packet->m_friendsOnlyUGC) ++server->m_ugcPlayersVersion;
@ -536,7 +539,15 @@ if (player->riding != nullptr)
{ {
players.erase(it); players.erase(it);
} }
//broadcastAll(shared_ptr<PlayerInfoPacket>( new PlayerInfoPacket(player->name, false, 9999) ) ); // Notify all remaining clients that this player left.
// The original PlayerInfoPacket removal was commented out (repurposed by 4J).
// Send a RemoveEntitiesPacket so clients remove the player entity from their world
// and from the tab/player list display.
{
intArray ids(1);
ids[0] = player->entityId;
broadcastAll(std::make_shared<RemoveEntitiesPacket>(ids));
}
removePlayerFromReceiving(player); removePlayerFromReceiving(player);
player->connection = nullptr; // Must remove reference to connection, or else there is a circular dependency player->connection = nullptr; // Must remove reference to connection, or else there is a circular dependency
@ -547,7 +558,7 @@ if (player->riding != nullptr)
saveAll(nullptr,false); saveAll(nullptr,false);
} }
shared_ptr<ServerPlayer> PlayerList::getPlayerForLogin(PendingConnection *pendingConnection, const wstring& userName, PlayerUID xuid, PlayerUID onlineXuid) shared_ptr<ServerPlayer> PlayerList::getPlayerForLogin(PendingConnection *pendingConnection, const wstring& userName, PlayerUID xuid)
{ {
if (players.size() >= (unsigned int)maxPlayers) if (players.size() >= (unsigned int)maxPlayers)
{ {
@ -556,27 +567,7 @@ shared_ptr<ServerPlayer> PlayerList::getPlayerForLogin(PendingConnection *pendin
} }
shared_ptr<ServerPlayer> player = std::make_shared<ServerPlayer>(server, server->getLevel(0), userName, new ServerPlayerGameMode(server->getLevel(0))); shared_ptr<ServerPlayer> player = std::make_shared<ServerPlayer>(server, server->getLevel(0), userName, new ServerPlayerGameMode(server->getLevel(0)));
player->gameMode->player = player; // 4J added as had to remove this assignment from ServerPlayer ctor player->gameMode->player = player; // 4J added as had to remove this assignment from ServerPlayer ctor
player->setXuid( xuid ); // 4J Added player->setXuid( xuid );
player->setOnlineXuid( onlineXuid ); // 4J Added
#ifdef _WINDOWS64
{
// Use packet-supplied identity from LoginPacket.
// Do not recompute from name here: mixed-version clients must stay compatible.
INetworkPlayer* np = pendingConnection->connection->getSocket()->getPlayer();
if (np != nullptr)
{
player->setOnlineXuid(np->GetUID());
// Backward compatibility: when Minecraft.Client is hosting, keep the first
// host player on the legacy embedded host XUID (base + 0).
// This preserves pre-migration host playerdata in existing worlds.
if (np->IsHost())
{
player->setXuid(Win64Xuid::GetLegacyEmbeddedHostXuid());
}
}
}
#endif
// Work out the base server player settings // Work out the base server player settings
INetworkPlayer *networkPlayer = pendingConnection->connection->getSocket()->getPlayer(); INetworkPlayer *networkPlayer = pendingConnection->connection->getSocket()->getPlayer();
if(networkPlayer != nullptr && !networkPlayer->IsHost()) if(networkPlayer != nullptr && !networkPlayer->IsHost())
@ -664,7 +655,6 @@ shared_ptr<ServerPlayer> PlayerList::respawn(shared_ptr<ServerPlayer> serverPlay
DWORD playerIndex = serverPlayer->getPlayerIndex(); DWORD playerIndex = serverPlayer->getPlayerIndex();
PlayerUID playerXuid = serverPlayer->getXuid(); PlayerUID playerXuid = serverPlayer->getXuid();
PlayerUID playerOnlineXuid = serverPlayer->getOnlineXuid();
shared_ptr<ServerPlayer> player = std::make_shared<ServerPlayer>(server, server->getLevel(serverPlayer->dimension), serverPlayer->getName(), new ServerPlayerGameMode(server->getLevel(serverPlayer->dimension))); shared_ptr<ServerPlayer> player = std::make_shared<ServerPlayer>(server, server->getLevel(serverPlayer->dimension), serverPlayer->getName(), new ServerPlayerGameMode(server->getLevel(serverPlayer->dimension)));
player->connection = serverPlayer->connection; player->connection = serverPlayer->connection;
@ -676,7 +666,6 @@ shared_ptr<ServerPlayer> PlayerList::respawn(shared_ptr<ServerPlayer> serverPlay
} }
player->gameMode->player = player; // 4J added as had to remove this assignment from ServerPlayer ctor player->gameMode->player = player; // 4J added as had to remove this assignment from ServerPlayer ctor
player->setXuid( playerXuid ); // 4J Added player->setXuid( playerXuid ); // 4J Added
player->setOnlineXuid( playerOnlineXuid ); // 4J Added
// 4J Stu - Don't reuse the id. If we do, then the player can be re-added after being removed, but the add packet gets sent before the remove packet // 4J Stu - Don't reuse the id. If we do, then the player can be re-added after being removed, but the add packet gets sent before the remove packet
//player->entityId = serverPlayer->entityId; //player->entityId = serverPlayer->entityId;
@ -686,6 +675,8 @@ shared_ptr<ServerPlayer> PlayerList::respawn(shared_ptr<ServerPlayer> serverPlay
player->setPlayerIndex( playerIndex ); player->setPlayerIndex( playerIndex );
player->setCustomSkin( serverPlayer->getCustomSkin() ); player->setCustomSkin( serverPlayer->getCustomSkin() );
player->setCustomCape( serverPlayer->getCustomCape() ); player->setCustomCape( serverPlayer->getCustomCape() );
player->customTextureUrl = serverPlayer->customTextureUrl;
player->customTextureUrl2 = serverPlayer->customTextureUrl2;
player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, serverPlayer->getAllPlayerGamePrivileges()); player->setPlayerGamePrivilege(Player::ePlayerGamePrivilege_All, serverPlayer->getAllPlayerGamePrivileges());
player->gameMode->setGameRules( serverPlayer->gameMode->getGameRules() ); player->gameMode->setGameRules( serverPlayer->gameMode->getGameRules() );
player->dimension = targetDimension; player->dimension = targetDimension;
@ -1012,6 +1003,12 @@ void PlayerList::tick()
} }
#ifdef _WINDOWS64 #ifdef _WINDOWS64
// Kill any zombie PendingConnections that still reference this smallId.
// Their Socket::getPlayer() resolves by smallId, so if we recycle the id
// before they are gone, a stale timeout would disconnect the new player.
if (server->connection != nullptr)
server->connection->closePendingConnectionsBySmallId(smallId);
// The old Connection's read/write threads are now dead (disconnect waits // The old Connection's read/write threads are now dead (disconnect waits
// for them). Safe to recycle the smallId — no stale write thread can // for them). Safe to recycle the smallId — no stale write thread can
// resolve getPlayer() to a new connection that reuses this slot. // resolve getPlayer() to a new connection that reuses this slot.
@ -1039,7 +1036,7 @@ void PlayerList::tick()
for(unsigned int i = 0; i < players.size(); i++) for(unsigned int i = 0; i < players.size(); i++)
{ {
shared_ptr<ServerPlayer> p = players.at(i); shared_ptr<ServerPlayer> p = players.at(i);
PlayerUID playersXuid = p->getOnlineXuid(); PlayerUID playersXuid = p->getXuid();
if (p != nullptr && ProfileManager.AreXUIDSEqual(playersXuid, xuid ) ) if (p != nullptr && ProfileManager.AreXUIDSEqual(playersXuid, xuid ) )
{ {
player = p; player = p;
@ -1049,7 +1046,7 @@ void PlayerList::tick()
if (player != nullptr) if (player != nullptr)
{ {
m_bannedXuids.push_back( player->getOnlineXuid() ); m_bannedXuids.push_back( player->getXuid() );
// 4J Stu - If we have kicked a player, make sure that they have no privileges if they later try to join the world when trust players is off // 4J Stu - If we have kicked a player, make sure that they have no privileges if they later try to join the world when trust players is off
player->enableAllPlayerPrivileges( false ); player->enableAllPlayerPrivileges( false );
player->connection->setWasKicked(); player->connection->setWasKicked();
@ -1062,7 +1059,7 @@ void PlayerList::tick()
LeaveCriticalSection(&m_kickPlayersCS); LeaveCriticalSection(&m_kickPlayersCS);
// Check our receiving players, and if they are dead see if we can replace them // Check our receiving players, and if they are dead see if we can replace them
for(unsigned int dim = 0; dim < 2; ++dim) for(unsigned int dim = 0; dim < DIMENSION_COUNT; ++dim)
{ {
for(unsigned int i = 0; i < receiveAllPlayers[dim].size(); ++i) for(unsigned int i = 0; i < receiveAllPlayers[dim].size(); ++i)
{ {
@ -1160,7 +1157,7 @@ shared_ptr<ServerPlayer> PlayerList::getPlayer(PlayerUID uid)
for (unsigned int i = 0; i < players.size(); i++) for (unsigned int i = 0; i < players.size(); i++)
{ {
shared_ptr<ServerPlayer> p = players[i]; shared_ptr<ServerPlayer> p = players[i];
if (p->getXuid() == uid || p->getOnlineXuid() == uid) // 4J - used to be case insensitive (using equalsIgnoreCase) - imagine we'll be shifting to XUIDs anyway if (p->getXuid() == uid)
{ {
return p; return p;
} }

View file

@ -52,7 +52,8 @@ private:
int sendAllPlayerInfoIn; int sendAllPlayerInfoIn;
// 4J Added to maintain which players in which dimensions can receive all packet types // 4J Added to maintain which players in which dimensions can receive all packet types
vector<shared_ptr<ServerPlayer> > receiveAllPlayers[3]; static const int DIMENSION_COUNT = 3; // overworld / nether / end — keep in sync with LevelData
vector<shared_ptr<ServerPlayer> > receiveAllPlayers[DIMENSION_COUNT];
private: private:
shared_ptr<ServerPlayer> findAlivePlayerOnSystem(shared_ptr<ServerPlayer> currentPlayer); shared_ptr<ServerPlayer> findAlivePlayerOnSystem(shared_ptr<ServerPlayer> currentPlayer);
@ -81,7 +82,7 @@ public:
void add(shared_ptr<ServerPlayer> player); void add(shared_ptr<ServerPlayer> player);
void move(shared_ptr<ServerPlayer> player); void move(shared_ptr<ServerPlayer> player);
void remove(shared_ptr<ServerPlayer> player); void remove(shared_ptr<ServerPlayer> player);
shared_ptr<ServerPlayer> getPlayerForLogin(PendingConnection *pendingConnection, const wstring& userName, PlayerUID xuid, PlayerUID OnlineXuid); shared_ptr<ServerPlayer> getPlayerForLogin(PendingConnection *pendingConnection, const wstring& userName, PlayerUID xuid);
shared_ptr<ServerPlayer> respawn(shared_ptr<ServerPlayer> serverPlayer, int targetDimension, bool keepAllPlayerData); shared_ptr<ServerPlayer> respawn(shared_ptr<ServerPlayer> serverPlayer, int targetDimension, bool keepAllPlayerData);
void toggleDimension(shared_ptr<ServerPlayer> player, int targetDimension); void toggleDimension(shared_ptr<ServerPlayer> player, int targetDimension);
void repositionAcrossDimension(shared_ptr<Entity> entity, int lastDimension, ServerLevel *oldLevel, ServerLevel *newLevel); void repositionAcrossDimension(shared_ptr<Entity> entity, int lastDimension, ServerLevel *oldLevel, ServerLevel *newLevel);

View file

@ -44,6 +44,23 @@ void ServerConnection::handleConnection(shared_ptr<PendingConnection> uc)
LeaveCriticalSection(&pending_cs); LeaveCriticalSection(&pending_cs);
} }
void ServerConnection::closePendingConnectionsBySmallId(BYTE smallId)
{
EnterCriticalSection(&pending_cs);
for (auto& pc : pending)
{
if (pc && !pc->done && pc->connection != nullptr)
{
Socket *pcSocket = pc->connection->getSocket();
if (pcSocket != nullptr && pcSocket->getSmallId() == smallId)
{
pc->done = true;
}
}
}
LeaveCriticalSection(&pending_cs);
}
void ServerConnection::stop() void ServerConnection::stop()
{ {
std::vector<shared_ptr<PendingConnection> > pendingSnapshot; std::vector<shared_ptr<PendingConnection> > pendingSnapshot;

View file

@ -40,6 +40,7 @@ private:
public: public:
void stop(); void stop();
void tick(); void tick();
void closePendingConnectionsBySmallId(BYTE smallId);
// 4J Added // 4J Added
bool addPendingTextureRequest(const wstring &textureName); bool addPendingTextureRequest(const wstring &textureName);

View file

@ -290,8 +290,10 @@ public:
public: public:
void clearLastBoundId(); void clearLastBoundId();
private: public:
int loadTexture(TEXTURE_NAME texId, const wstring& resourceName); int loadTexture(TEXTURE_NAME texId, const wstring& resourceName);
// Narrow public accessor for ad-hoc path-based texture loading (used by NativeUIRenderer)
int loadTextureByPath(const wstring& resourceName) { return loadTexture(TN_COUNT, resourceName); }
public: public:
int loadTexture(int idx); // 4J added int loadTexture(int idx); // 4J added
int getTexture(BufferedImage *img, C4JRender::eTextureFormat format = C4JRender::TEXTURE_FORMAT_RxGyBzAw, bool mipmap = true); int getTexture(BufferedImage *img, C4JRender::eTextureFormat format = C4JRender::TEXTURE_FORMAT_RxGyBzAw, bool mipmap = true);

View file

@ -546,13 +546,12 @@ void TrackedEntity::updatePlayer(EntityTracker *tracker, shared_ptr<ServerPlayer
{ {
shared_ptr<LivingEntity> living = dynamic_pointer_cast<LivingEntity>(e); shared_ptr<LivingEntity> living = dynamic_pointer_cast<LivingEntity>(e);
ServersideAttributeMap *attributeMap = static_cast<ServersideAttributeMap *>(living->getAttributes()); ServersideAttributeMap *attributeMap = static_cast<ServersideAttributeMap *>(living->getAttributes());
unordered_set<AttributeInstance *> *attributes = attributeMap->getSyncableAttributes(); std::unique_ptr<unordered_set<AttributeInstance *>> attributes(attributeMap->getSyncableAttributes());
if (!attributes->empty()) if (!attributes->empty())
{ {
sp->connection->send(std::make_shared<UpdateAttributesPacket>(e->entityId, attributes)); sp->connection->send(std::make_shared<UpdateAttributesPacket>(e->entityId, attributes.get()));
} }
delete attributes;
} }
if (trackDelta && !isAddMobPacket) if (trackDelta && !isAddMobPacket)
@ -652,14 +651,11 @@ shared_ptr<Packet> TrackedEntity::getAddEntityPacket()
shared_ptr<ServerPlayer> player = dynamic_pointer_cast<ServerPlayer>(e); shared_ptr<ServerPlayer> player = dynamic_pointer_cast<ServerPlayer>(e);
PlayerUID xuid = INVALID_XUID; PlayerUID xuid = INVALID_XUID;
PlayerUID OnlineXuid = INVALID_XUID;
if( player != nullptr ) if( player != nullptr )
{ {
xuid = player->getXuid(); xuid = player->getXuid();
OnlineXuid = player->getOnlineXuid();
} }
// 4J Added yHeadRotp param to fix #102563 - TU12: Content: Gameplay: When one of the Players is idle for a few minutes his head turns 180 degrees. return std::make_shared<AddPlayerPacket>(player, xuid, xp, yp, zp, yRotp, xRotp, yHeadRotp);
return std::make_shared<AddPlayerPacket>(player, xuid, OnlineXuid, xp, yp, zp, yRotp, xRotp, yHeadRotp);
} }
else if (e->instanceof(eTYPE_MINECART)) else if (e->instanceof(eTYPE_MINECART))
{ {

View file

@ -1,6 +1,7 @@
#include "stdafx.h" #include "stdafx.h"
#include "..\Common\Consoles_App.h" #include "..\Common\Consoles_App.h"
#include "..\User.h" #include "..\User.h"
#include "..\..\MCAuth\include\MCAuthManager.h"
#include "..\..\Minecraft.Client\Minecraft.h" #include "..\..\Minecraft.Client\Minecraft.h"
#include "..\..\Minecraft.Client\MinecraftServer.h" #include "..\..\Minecraft.Client\MinecraftServer.h"
#include "..\..\Minecraft.Client\PlayerList.h" #include "..\..\Minecraft.Client\PlayerList.h"
@ -75,8 +76,22 @@ void CConsoleMinecraftApp::TemporaryCreateGameStart()
Minecraft *pMinecraft=Minecraft::GetInstance(); Minecraft *pMinecraft=Minecraft::GetInstance();
app.ReleaseSaveThumbnail(); app.ReleaseSaveThumbnail();
ProfileManager.SetLockedProfile(0); ProfileManager.SetLockedProfile(0);
extern wchar_t g_Win64UsernameW[17]; // Wait for auth to settle, then set user->name from session.
pMinecraft->user->name = g_Win64UsernameW; {
auto& mgr = MCAuthManager::Get();
MCAuthManager::State st = mgr.GetSlotState(0);
if (st == MCAuthManager::State::Authenticating || st == MCAuthManager::State::WaitingForCode)
{
app.DebugPrintf("[Auth] Waiting for token refresh before world entry...\n");
mgr.WaitForSlotReady(0, 15000);
}
if (mgr.IsSlotLoggedIn(0))
{
auto session = mgr.GetSlotSession(0);
if (!session.username.empty())
pMinecraft->user->name = std::wstring(session.username.begin(), session.username.end());
}
}
app.ApplyGameSettingsChanged(0); app.ApplyGameSettingsChanged(0);
////////////////////////////////////////////////////////////////////////////////////////////// From CScene_MultiGameJoinLoad::OnInit ////////////////////////////////////////////////////////////////////////////////////////////// From CScene_MultiGameJoinLoad::OnInit
@ -132,6 +147,8 @@ void CConsoleMinecraftApp::TemporaryCreateGameStart()
C4JThread* thread = new C4JThread(loadingParams->func, loadingParams->lpParam, "RunNetworkGame"); C4JThread* thread = new C4JThread(loadingParams->func, loadingParams->lpParam, "RunNetworkGame");
thread->Run(); thread->Run();
delete loadingParams;
} }
int CConsoleMinecraftApp::GetLocalTMSFileIndex(WCHAR *wchTMSFile,bool bFilenameIncludesExtension,eFileExtensionType eEXT) int CConsoleMinecraftApp::GetLocalTMSFileIndex(WCHAR *wchTMSFile,bool bFilenameIncludesExtension,eFileExtensionType eEXT)

View file

@ -46,7 +46,7 @@
#include "Common/PostProcesser.h" #include "Common/PostProcesser.h"
#include "..\GameRenderer.h" #include "..\GameRenderer.h"
#include "Network\WinsockNetLayer.h" #include "Network\WinsockNetLayer.h"
#include "Windows64_Xuid.h" #include "Windows64_Uuid.h"
#include "Common/UI/UI.h" #include "Common/UI/UI.h"
// Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib. // Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib.
@ -1302,6 +1302,13 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine); UNREFERENCED_PARAMETER(lpCmdLine);
// Allocate a console window so DebugPrintf output is visible when
// launching from a terminal (or attach to the parent console).
if (!AttachConsole(ATTACH_PARENT_PROCESS))
AllocConsole();
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
// 4J-Win64: set CWD to exe dir so asset paths resolve correctly // 4J-Win64: set CWD to exe dir so asset paths resolve correctly
{ {
char szExeDir[MAX_PATH] = {}; char szExeDir[MAX_PATH] = {};
@ -1319,50 +1326,19 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
g_rScreenWidth = GetSystemMetrics(SM_CXSCREEN); g_rScreenWidth = GetSystemMetrics(SM_CXSCREEN);
g_rScreenHeight = GetSystemMetrics(SM_CYSCREEN); g_rScreenHeight = GetSystemMetrics(SM_CYSCREEN);
// Load username from username.txt // username.txt is DEPRECATED — player identity comes from MCAuthManager.
char exePath[MAX_PATH] = {}; // The -name launch arg is still supported as a pre-auth default.
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
char *lastSlash = strrchr(exePath, '\\');
if (lastSlash)
{
*(lastSlash + 1) = '\0';
}
char filePath[MAX_PATH] = {}; // Load stuff from launch options, including -name override
_snprintf_s(filePath, sizeof(filePath), _TRUNCATE, "%susername.txt", exePath);
FILE *f = nullptr;
if (fopen_s(&f, filePath, "r") == 0 && f)
{
char buf[128] = {};
if (fgets(buf, sizeof(buf), f))
{
int len = static_cast<int>(strlen(buf));
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r' || buf[len - 1] == ' '))
{
buf[--len] = '\0';
}
if (len > 0)
{
strncpy_s(g_Win64Username, sizeof(g_Win64Username), buf, _TRUNCATE);
}
}
fclose(f);
}
// Load stuff from launch options, including username
const Win64LaunchOptions launchOptions = ParseLaunchOptions(); const Win64LaunchOptions launchOptions = ParseLaunchOptions();
ApplyScreenMode(launchOptions.screenMode); ApplyScreenMode(launchOptions.screenMode);
// Ensure uid.dat exists from startup (before any multiplayer/login path). // Player identity is resolved by MCAuthManager during login — no local uid.dat needed.
Win64Xuid::ResolvePersistentXuid();
// If no username, let's fall back // If no username, fall back to "Player"
if (g_Win64Username[0] == 0) if (g_Win64Username[0] == 0)
{ {
// Default username will be "Player" strncpy_s(g_Win64Username, sizeof(g_Win64Username), "Player", _TRUNCATE);
strncpy_s(g_Win64Username, sizeof(g_Win64Username), "Player", _TRUNCATE);
} }
MultiByteToWideChar(CP_ACP, 0, g_Win64Username, -1, g_Win64UsernameW, 17); MultiByteToWideChar(CP_ACP, 0, g_Win64Username, -1, g_Win64UsernameW, 17);
@ -1644,7 +1620,18 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
{ {
pMinecraft->applyFrameMouseLook(); // Per-frame mouse look (before ticks + render) pMinecraft->applyFrameMouseLook(); // Per-frame mouse look (before ticks + render)
pMinecraft->run_middle(); pMinecraft->run_middle();
app.SetAppPaused( g_NetworkManager.IsLocalGame() && g_NetworkManager.GetPlayerCount() == 1 && ui.IsPauseMenuDisplayed(ProfileManager.GetPrimaryPad()) ); {
bool shouldPause = false;
// Freeze ticks ONLY in local single player with pause menu open.
// Never freeze online — the server keeps running regardless.
if (g_NetworkManager.IsLocalGame() && g_NetworkManager.GetPlayerCount() == 1
&& ui.IsPauseMenuDisplayed(ProfileManager.GetPrimaryPad()))
shouldPause = true;
// Also freeze ticks in local game when account picker is open
if (g_NetworkManager.IsLocalGame() && pMinecraft->isAnySplitAuthUIOpen())
shouldPause = true;
app.SetAppPaused(shouldPause);
}
} }
else else
{ {

View file

@ -4,6 +4,8 @@
// Temp // Temp
#include "..\Minecraft.h" #include "..\Minecraft.h"
#include "..\Textures.h" #include "..\Textures.h"
#include "..\Font.h"
#include "..\Tesselator.h"
#define _ENABLEIGGY #define _ENABLEIGGY

View file

@ -0,0 +1,42 @@
#pragma once
#ifdef _WINDOWS64
#include <string>
#include <Windows.h>
#include "..\..\Minecraft.World\GameUUID.h"
// Player identity is always assigned by the auth manager (Mojang/offline).
// No local persistence (uuid.dat) is used — the auth handshake is the source of truth.
namespace Win64Uuid
{
inline GameUUID DeriveUuidForPad(GameUUID baseUuid, int iPad)
{
if (iPad == 0)
return baseUuid;
// Deterministic per-pad UUID derived from the base.
uint64_t mixHi = baseUuid.hi ^ (0x9E3779B97F4A7C15ULL * (uint64_t)(iPad + 1));
uint64_t mixLo = baseUuid.lo ^ (0xBF58476D1CE4E5B9ULL * (uint64_t)(iPad + 1));
mixHi = (mixHi ^ (mixHi >> 30)) * 0xBF58476D1CE4E5B9ULL;
mixHi = (mixHi ^ (mixHi >> 27)) * 0x94D049BB133111EBULL;
mixHi = mixHi ^ (mixHi >> 31);
mixLo = (mixLo ^ (mixLo >> 30)) * 0xBF58476D1CE4E5B9ULL;
mixLo = (mixLo ^ (mixLo >> 27)) * 0x94D049BB133111EBULL;
mixLo = mixLo ^ (mixLo >> 31);
// Mark as UUID v4 variant 1
mixHi = (mixHi & ~0x000000000000F000ULL) | 0x0000000000004000ULL;
mixLo = (mixLo & ~0xC000000000000000ULL) | 0x8000000000000000ULL;
GameUUID derived;
derived.hi = mixHi;
derived.lo = mixLo;
return derived;
}
}
#endif

View file

@ -1,237 +0,0 @@
#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(uint64_t))
{
uint64_t 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;
uint64_t 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 uint64_t Mix64(uint64_t 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);
uint64_t t = (((uint64_t)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
LARGE_INTEGER qpc = {};
QueryPerformanceCounter(&qpc);
uint64_t seed = t;
seed ^= (uint64_t)qpc.QuadPart;
seed ^= ((uint64_t)GetCurrentProcessId() << 32);
seed ^= (uint64_t)GetCurrentThreadId();
seed ^= (uint64_t)GetTickCount();
seed ^= (uint64_t)(size_t)&qpc;
seed ^= (uint64_t)(size_t)GetModuleHandleA(NULL);
uint64_t 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 DeriveXuidForPad(PlayerUID baseXuid, int iPad)
{
if (iPad == 0)
return baseXuid;
// Deterministic per-pad XUID: hash the base XUID with the pad number.
// Produces a fully unique 64-bit value with no risk of overlap.
// Suggested by rtm516 to avoid adjacent-integer collisions from the old "+ iPad" approach.
uint64_t raw = Mix64((uint64_t)baseXuid + (uint64_t)iPad);
raw |= 0x8000000000000000ULL; // keep high bit set like all our XUIDs
PlayerUID xuid = (PlayerUID)raw;
if (!IsPersistedUidValid(xuid))
{
raw ^= 0x0100000000000001ULL;
xuid = (PlayerUID)raw;
}
if (!IsPersistedUidValid(xuid))
xuid = (PlayerUID)(0xD15EA5E000000001ULL + iPad);
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

View file

@ -748,7 +748,7 @@ bool CPlatformNetworkManagerXbox::_LeaveGame(bool bMigrateHost, bool bLeaveRoom)
return true; return true;
} }
void CPlatformNetworkManagerXbox::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerXbox::HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
// #ifdef _XBOX // #ifdef _XBOX
// 4J Stu - We probably did this earlier as well, but just to be sure! // 4J Stu - We probably did this earlier as well, but just to be sure!
@ -763,7 +763,7 @@ void CPlatformNetworkManagerXbox::HostGame(int localUsersMask, bool bOnlineGame,
//#endif //#endif
} }
void CPlatformNetworkManagerXbox::_HostGame(int usersMask, unsigned char publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, unsigned char privateSlots /*= 0*/) void CPlatformNetworkManagerXbox::_HostGame(int usersMask, int publicSlots /*= MINECRAFT_NET_MAX_PLAYERS*/, int privateSlots /*= 0*/)
{ {
HRESULT hr; HRESULT hr;
// Create a session using the standard game type, in multiplayer game mode, // Create a session using the standard game type, in multiplayer game mode,

View file

@ -47,7 +47,7 @@ public:
virtual void SendInviteGUI(int quadrant); virtual void SendInviteGUI(int quadrant);
virtual bool IsAddingPlayer(); virtual bool IsAddingPlayer();
virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void HostGame(int localUsersMask, bool bOnlineGame, bool bIsPrivate, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex ); virtual int JoinGame(FriendSessionInfo *searchResult, int localUsersMask, int primaryUserIndex );
virtual bool SetLocalGame(bool isLocal); virtual bool SetLocalGame(bool isLocal);
virtual bool IsLocalGame() { return m_bIsOfflineGame; } virtual bool IsLocalGame() { return m_bIsOfflineGame; }
@ -66,7 +66,7 @@ public:
private: private:
bool isSystemPrimaryPlayer(IQNetPlayer *pQNetPlayer); bool isSystemPrimaryPlayer(IQNetPlayer *pQNetPlayer);
virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom); virtual bool _LeaveGame(bool bMigrateHost, bool bLeaveRoom);
virtual void _HostGame(int dwUsersMask, unsigned char publicSlots = MINECRAFT_NET_MAX_PLAYERS, unsigned char privateSlots = 0); virtual void _HostGame(int dwUsersMask, int publicSlots = MINECRAFT_NET_MAX_PLAYERS, int privateSlots = 0);
virtual bool _StartGame(); virtual bool _StartGame();
IQNet * m_pIQNet; // pointer to QNet interface IQNet * m_pIQNet; // pointer to QNet interface

View file

@ -19,7 +19,7 @@ public:
WCHAR wNameXUID[32] = L""; WCHAR wNameXUID[32] = L"";
WCHAR wNameSkin[32] = L""; WCHAR wNameSkin[32] = L"";
WCHAR wNameCloak[32] = L""; WCHAR wNameCloak[32] = L"";
PlayerUID xuid=0LL; PlayerUID xuid = INVALID_XUID;
if (NameLen >31) if (NameLen >31)
@ -47,7 +47,12 @@ public:
{ {
ZeroMemory(wTemp,sizeof(WCHAR)*35); ZeroMemory(wTemp,sizeof(WCHAR)*35);
wcsncpy_s( wTemp, pAttributes[i].strValue, pAttributes[i].ValueLen); wcsncpy_s( wTemp, pAttributes[i].strValue, pAttributes[i].ValueLen);
xuid=_wcstoui64(wTemp,nullptr,10); // Parse UUID from wide string (dashed format)
{
char narrow[64] = {};
wcstombs_s(nullptr, narrow, wTemp, _TRUNCATE);
xuid = PlayerUID::fromDashed(std::string(narrow));
}
} }
} }
else if (_wcsicmp(wAttName,L"cape")==0) else if (_wcsicmp(wAttName,L"cape")==0)
@ -68,7 +73,7 @@ public:
} }
// if the xuid hasn't been defined, then we can't use the data // if the xuid hasn't been defined, then we can't use the data
if(xuid!=0LL) if(xuid.isValid())
{ {
return CConsoleMinecraftApp::RegisterMojangData(wNameXUID , xuid, wNameSkin, wNameCloak ); return CConsoleMinecraftApp::RegisterMojangData(wNameXUID , xuid, wNameSkin, wNameCloak );
} }

View file

@ -19,6 +19,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_NETWORK
source_group("Common/Network" FILES ${_MINECRAFT_CLIENT_WINDOWS_COMMON_NETWORK}) source_group("Common/Network" FILES ${_MINECRAFT_CLIENT_WINDOWS_COMMON_NETWORK})
set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/NativeUIRenderer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/NativeUIRenderer.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UI.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UI.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIBitmapFont.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIBitmapFont.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIBitmapFont.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIBitmapFont.h"
@ -167,6 +169,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI_SCENES_FRONTEND_MENU_SCREENS
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LoadOrJoinMenu.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LoadOrJoinMenu.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MainMenu.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MainMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MainMenu.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MainMenu.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MSAuth.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_MSAuth.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_NewUpdateMessage.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_NewUpdateMessage.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_NewUpdateMessage.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_NewUpdateMessage.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_SaveMessage.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_SaveMessage.cpp"
@ -354,6 +358,7 @@ set(_MINECRAFT_CLIENT_WINDOWS_WINDOWS64
"${BASE_DIR}/Minecraft_Macros.h" "${BASE_DIR}/Minecraft_Macros.h"
"${BASE_DIR}/PostProcesser.cpp" "${BASE_DIR}/PostProcesser.cpp"
"${BASE_DIR}/Windows64_Minecraft.cpp" "${BASE_DIR}/Windows64_Minecraft.cpp"
"${BASE_DIR}/Windows64_Uuid.h"
) )
source_group("Windows64" FILES ${_MINECRAFT_CLIENT_WINDOWS_WINDOWS64}) source_group("Windows64" FILES ${_MINECRAFT_CLIENT_WINDOWS_WINDOWS64})

View file

@ -14,13 +14,6 @@ namespace ServerRuntime
{ {
namespace namespace
{ {
/**
* **Access State**
*
* These features are used extensively from various parts of the code, so safe read/write handling is implemented
* Stores the published BAN manager snapshot plus a writer gate for clone-and-publish updates
* BanManagerスナップショットと更新直列化用ロックを保持する
*/
struct AccessState struct AccessState
{ {
std::mutex stateLock; std::mutex stateLock;
@ -32,20 +25,12 @@ namespace ServerRuntime
AccessState g_accessState; AccessState g_accessState;
/**
* Copies the currently published manager pointer so readers can work without holding the publish mutex
* BanManager共有ポインタを複製取得する
*/
static std::shared_ptr<BanManager> GetBanManagerSnapshot() static std::shared_ptr<BanManager> GetBanManagerSnapshot()
{ {
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock); std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
return g_accessState.banManager; return g_accessState.banManager;
} }
/**
* Replaces the shared manager pointer with a fully prepared snapshot in one short critical section
* BanManagerスナップショットを短いロックで公開する
*/
static void PublishBanManagerSnapshot(const std::shared_ptr<BanManager> &banManager) static void PublishBanManagerSnapshot(const std::shared_ptr<BanManager> &banManager)
{ {
std::lock_guard<std::mutex> stateLock(g_accessState.stateLock); std::lock_guard<std::mutex> stateLock(g_accessState.stateLock);
@ -71,10 +56,7 @@ namespace ServerRuntime
{ {
return ""; return "";
} }
return xuid.toDashed();
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", (unsigned long long)xuid);
return buffer;
} }
bool TryParseXuid(const std::string &text, PlayerUID *outXuid) bool TryParseXuid(const std::string &text, PlayerUID *outXuid)
@ -84,13 +66,13 @@ namespace ServerRuntime
return false; return false;
} }
unsigned long long parsed = 0; PlayerUID parsed = PlayerUID::fromDashed(text);
if (!StringUtils::TryParseUnsignedLongLong(text, &parsed) || parsed == 0ULL) if (!parsed.isValid())
{ {
return false; return false;
} }
*outXuid = (PlayerUID)parsed; *outXuid = parsed;
return true; return true;
} }

View file

@ -37,6 +37,7 @@ set_target_properties(Minecraft.Server PROPERTIES
target_link_libraries(Minecraft.Server PRIVATE target_link_libraries(Minecraft.Server PRIVATE
Minecraft.World Minecraft.World
MCAuth
d3d11 d3d11
d3dcompiler d3dcompiler
XInput9_1_0 XInput9_1_0

View file

@ -2,6 +2,7 @@
#include "FileUtils.h" #include "FileUtils.h"
#include "StringUtils.h" #include "StringUtils.h"
#include "..\..\Minecraft.World\GameUUID.h"
#include "..\vendor\nlohmann\json.hpp" #include "..\vendor\nlohmann\json.hpp"
@ -53,19 +54,21 @@ namespace ServerRuntime
return ""; return "";
} }
unsigned long long numericXuid = 0; // Try parsing as a dashed 128-bit UUID (standard format)
if (StringUtils::TryParseUnsignedLongLong(trimmed, &numericXuid)) GameUUID parsed = GameUUID::fromDashed(trimmed);
if (parsed.isValid())
{ {
if (numericXuid == 0ULL) return parsed.toDashed();
{
return "";
}
char buffer[32] = {};
sprintf_s(buffer, sizeof(buffer), "0x%016llx", numericXuid);
return buffer;
} }
// Try parsing as an undashed 128-bit UUID
parsed = GameUUID::fromUndashed(trimmed);
if (parsed.isValid())
{
return parsed.toDashed();
}
// Fallback: lowercase whatever was passed (e.g. player names in legacy files)
return StringUtils::ToLowerAscii(trimmed); return StringUtils::ToLowerAscii(trimmed);
} }

View file

@ -36,9 +36,7 @@ namespace ServerRuntime
return; return;
} }
// Keep both identity variants because the dedicated server checks login and online XUIDs separately.
AppendUniqueXuid(player->getXuid(), out); AppendUniqueXuid(player->getXuid(), out);
AppendUniqueXuid(player->getOnlineXuid(), out);
} }
} }

View file

@ -82,10 +82,6 @@ namespace ServerRuntime
{ {
AppendUniqueXuid(onlineTarget->getXuid(), &xuidsToRemove); AppendUniqueXuid(onlineTarget->getXuid(), &xuidsToRemove);
} }
if (ServerRuntime::Access::IsPlayerBanned(onlineTarget->getOnlineXuid()))
{
AppendUniqueXuid(onlineTarget->getOnlineXuid(), &xuidsToRemove);
}
} }
std::vector<ServerRuntime::Access::BannedPlayerEntry> entries; std::vector<ServerRuntime::Access::BannedPlayerEntry> entries;

View file

@ -80,7 +80,9 @@ static const ServerPropertyDefault kServerPropertyDefaults[] =
{ "spawn-monsters", "true" }, { "spawn-monsters", "true" },
{ "spawn-npcs", "true" }, { "spawn-npcs", "true" },
{ "tnt", "true" }, { "tnt", "true" },
{ "trust-players", "true" } { "trust-players", "true" },
{ "online-mode", "true" },
{ "auth-provider", "mojang" }
}; };
static std::string BoolToString(bool value) static std::string BoolToString(bool value)
@ -398,6 +400,7 @@ static bool WriteServerPropertiesFile(const char *filePath, const std::unordered
std::string text; std::string text;
text += "# Minecraft server properties\n"; text += "# Minecraft server properties\n";
text += "# Auto-generated and normalized when missing\n"; text += "# Auto-generated and normalized when missing\n";
text += "# Currently supported auth-provider: [mojang, elyby]\n";
std::map<std::string, std::string> sortedProperties(properties.begin(), properties.end()); std::map<std::string, std::string> sortedProperties(properties.begin(), properties.end());
for (std::map<std::string, std::string>::const_iterator it = sortedProperties.begin(); it != sortedProperties.end(); ++it) for (std::map<std::string, std::string>::const_iterator it = sortedProperties.begin(); it != sortedProperties.end(); ++it)
@ -826,7 +829,7 @@ ServerPropertiesConfig LoadServerPropertiesConfig()
config.autosaveIntervalSeconds = ReadNormalizedIntProperty(&merged, "autosave-interval", kDefaultAutosaveIntervalSeconds, 5, 3600, &shouldWrite); config.autosaveIntervalSeconds = ReadNormalizedIntProperty(&merged, "autosave-interval", kDefaultAutosaveIntervalSeconds, 5, 3600, &shouldWrite);
config.difficulty = ReadNormalizedIntProperty(&merged, "difficulty", 1, 0, 3, &shouldWrite); config.difficulty = ReadNormalizedIntProperty(&merged, "difficulty", 1, 0, 3, &shouldWrite);
config.gameMode = ReadNormalizedIntProperty(&merged, "gamemode", 0, 0, 1, &shouldWrite); config.gameMode = ReadNormalizedIntProperty(&merged, "gamemode", 0, 0, 2, &shouldWrite);
config.worldSize = ReadNormalizedWorldSizeProperty( config.worldSize = ReadNormalizedWorldSizeProperty(
&merged, &merged,
"world-size", "world-size",
@ -861,6 +864,14 @@ ServerPropertiesConfig LoadServerPropertiesConfig()
config.doTileDrops = ReadNormalizedBoolProperty(&merged, "do-tile-drops", true, &shouldWrite); config.doTileDrops = ReadNormalizedBoolProperty(&merged, "do-tile-drops", true, &shouldWrite);
config.naturalRegeneration = ReadNormalizedBoolProperty(&merged, "natural-regeneration", true, &shouldWrite); config.naturalRegeneration = ReadNormalizedBoolProperty(&merged, "natural-regeneration", true, &shouldWrite);
config.doDaylightCycle = ReadNormalizedBoolProperty(&merged, "do-daylight-cycle", true, &shouldWrite); config.doDaylightCycle = ReadNormalizedBoolProperty(&merged, "do-daylight-cycle", true, &shouldWrite);
config.onlineMode = ReadNormalizedBoolProperty(&merged, "online-mode", true, &shouldWrite);
config.authProvider = ReadNormalizedStringProperty(&merged, "auth-provider", "mojang", 16, &shouldWrite);
if (config.authProvider != "mojang" && config.authProvider != "elyby")
{
config.authProvider = "mojang";
merged["auth-provider"] = "mojang";
shouldWrite = true;
}
config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite); config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite);
config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite); config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite);

View file

@ -74,6 +74,11 @@ namespace ServerRuntime
bool naturalRegeneration; bool naturalRegeneration;
bool doDaylightCycle; bool doDaylightCycle;
/** `online-mode` — require Mojang session verification */
bool onlineMode;
/** `auth-provider` — which auth provider to use: "mojang" or "elyby" */
std::string authProvider = "mojang";
/** other MinecraftServer runtime settings */ /** other MinecraftServer runtime settings */
int maxBuildHeight; int maxBuildHeight;
std::string levelType; std::string levelType;

View file

@ -410,6 +410,8 @@ int main(int argc, char **argv)
LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled"); LogInfof("startup", "LAN advertise: %s", serverProperties.lanAdvertise ? "enabled" : "disabled");
LogInfof("startup", "Whitelist: %s", serverProperties.whiteListEnabled ? "enabled" : "disabled"); LogInfof("startup", "Whitelist: %s", serverProperties.whiteListEnabled ? "enabled" : "disabled");
LogInfof("startup", "Spawn protection radius: %d", serverProperties.spawnProtectionRadius); LogInfof("startup", "Spawn protection radius: %d", serverProperties.spawnProtectionRadius);
LogInfof("startup", "Online mode: %s", serverProperties.onlineMode ? "enabled" : "disabled");
LogInfof("startup", "Auth provider: %s", serverProperties.authProvider.c_str());
#ifdef _LARGE_WORLDS #ifdef _LARGE_WORLDS
LogInfof( LogInfof(
"startup", "startup",
@ -589,7 +591,7 @@ int main(int argc, char **argv)
param->dedicatedNoLocalHostPlayer = true; param->dedicatedNoLocalHostPlayer = true;
LogStartupStep("starting hosted network game thread"); LogStartupStep("starting hosted network game thread");
g_NetworkManager.HostGame(0, true, false, (unsigned char)config.maxPlayers, 0); g_NetworkManager.HostGame(0, true, false, config.maxPlayers, 0);
g_NetworkManager.FakeLocalPlayerJoined(); g_NetworkManager.FakeLocalPlayerJoined();
C4JThread *startThread = new C4JThread(&CGameNetworkManager::RunNetworkGameThreadProc, (LPVOID)param, "RunNetworkGame"); C4JThread *startThread = new C4JThread(&CGameNetworkManager::RunNetworkGameThreadProc, (LPVOID)param, "RunNetworkGame");

View file

@ -32,7 +32,7 @@ AddPlayerPacket::~AddPlayerPacket()
if(unpack != nullptr) delete unpack; if(unpack != nullptr) delete unpack;
} }
AddPlayerPacket::AddPlayerPacket(shared_ptr<Player> player, PlayerUID xuid, PlayerUID OnlineXuid,int xp, int yp, int zp, int yRotp, int xRotp, int yHeadRotp) AddPlayerPacket::AddPlayerPacket(shared_ptr<Player> player, PlayerUID xuid, int xp, int yp, int zp, int yRotp, int xRotp, int yHeadRotp)
{ {
id = player->entityId; id = player->entityId;
name = player->getName(); name = player->getName();
@ -54,7 +54,6 @@ AddPlayerPacket::AddPlayerPacket(shared_ptr<Player> player, PlayerUID xuid, Play
carriedItem = itemInstance == nullptr ? 0 : itemInstance->id; carriedItem = itemInstance == nullptr ? 0 : itemInstance->id;
this->xuid = xuid; this->xuid = xuid;
this->OnlineXuid = OnlineXuid;
m_playerIndex = static_cast<BYTE>(player->getPlayerIndex()); m_playerIndex = static_cast<BYTE>(player->getPlayerIndex());
m_skinId = player->getCustomSkin(); m_skinId = player->getCustomSkin();
m_capeId = player->getCustomCape(); m_capeId = player->getCustomCape();
@ -76,14 +75,13 @@ void AddPlayerPacket::read(DataInputStream *dis) //throws IOException
yHeadRot = dis->readByte(); // 4J Added yHeadRot = dis->readByte(); // 4J Added
carriedItem = dis->readShort(); carriedItem = dis->readShort();
xuid = dis->readPlayerUID(); xuid = dis->readPlayerUID();
OnlineXuid = dis->readPlayerUID();
m_playerIndex = dis->readByte(); m_playerIndex = dis->readByte();
INT skinId = dis->readInt(); INT skinId = dis->readInt();
m_skinId = *(DWORD *)&skinId; m_skinId = static_cast<DWORD>(skinId);
INT capeId = dis->readInt(); INT capeId = dis->readInt();
m_capeId = *(DWORD *)&capeId; m_capeId = static_cast<DWORD>(capeId);
INT privileges = dis->readInt(); INT privileges = dis->readInt();
m_uiGamePrivileges = *(unsigned int *)&privileges; m_uiGamePrivileges = static_cast<unsigned int>(privileges);
MemSect(1); MemSect(1);
unpack = SynchedEntityData::unpack(dis); unpack = SynchedEntityData::unpack(dis);
MemSect(0); MemSect(0);
@ -101,7 +99,6 @@ void AddPlayerPacket::write(DataOutputStream *dos) //throws IOException
dos->writeByte(yHeadRot); // 4J Added dos->writeByte(yHeadRot); // 4J Added
dos->writeShort(carriedItem); dos->writeShort(carriedItem);
dos->writePlayerUID(xuid); dos->writePlayerUID(xuid);
dos->writePlayerUID(OnlineXuid);
dos->writeByte(m_playerIndex); dos->writeByte(m_playerIndex);
dos->writeInt(m_skinId); dos->writeInt(m_skinId);
dos->writeInt(m_capeId); dos->writeInt(m_capeId);
@ -117,7 +114,7 @@ void AddPlayerPacket::handle(PacketListener *listener)
int AddPlayerPacket::getEstimatedSize() int AddPlayerPacket::getEstimatedSize()
{ {
int iSize= sizeof(int) + Player::MAX_NAME_LENGTH + sizeof(int) + sizeof(int) + sizeof(int) + sizeof(BYTE) + sizeof(BYTE) +sizeof(short) + sizeof(PlayerUID) + sizeof(PlayerUID) + sizeof(int) + sizeof(BYTE) + sizeof(unsigned int) + sizeof(byte); int iSize= sizeof(int) + Player::MAX_NAME_LENGTH + sizeof(int) + sizeof(int) + sizeof(int) + sizeof(BYTE) + sizeof(BYTE) +sizeof(short) + sizeof(PlayerUID) + sizeof(int) + sizeof(BYTE) + sizeof(unsigned int) + sizeof(byte);
if( entityData != nullptr ) if( entityData != nullptr )
{ {

View file

@ -20,7 +20,6 @@ public:
char yRot, xRot; char yRot, xRot;
int carriedItem; int carriedItem;
PlayerUID xuid; // 4J Added PlayerUID xuid; // 4J Added
PlayerUID OnlineXuid; // 4J Added
BYTE m_playerIndex; // 4J Added BYTE m_playerIndex; // 4J Added
DWORD m_skinId; // 4J Added DWORD m_skinId; // 4J Added
DWORD m_capeId; // 4J Added DWORD m_capeId; // 4J Added
@ -29,7 +28,7 @@ public:
AddPlayerPacket(); AddPlayerPacket();
~AddPlayerPacket(); ~AddPlayerPacket();
AddPlayerPacket(shared_ptr<Player> player, PlayerUID xuid, PlayerUID OnlineXuid,int xp, int yp, int zp, int yRotp, int xRotp, int yHeadRotp); AddPlayerPacket(shared_ptr<Player> player, PlayerUID xuid, int xp, int yp, int zp, int yRotp, int xRotp, int yHeadRotp);
virtual void read(DataInputStream *dis); virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos); virtual void write(DataOutputStream *dos);

View file

@ -0,0 +1,54 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthResponsePacket.h"
AuthResponsePacket::AuthResponsePacket()
{
}
AuthResponsePacket::AuthResponsePacket(const wstring& chosenScheme, const wstring& mojangUuid, const wstring& username)
{
this->chosenScheme = chosenScheme;
this->mojangUuid = mojangUuid;
this->username = username;
}
void AuthResponsePacket::read(DataInputStream *dis)
{
chosenScheme = readUtf(dis, 32);
mojangUuid = readUtf(dis, 64);
username = readUtf(dis, 16); // MC usernames are max 16 chars
if (chosenScheme != L"mojang" && chosenScheme != L"offline" && chosenScheme != L"elyby")
chosenScheme = L"";
// Validate username: only alphanumeric + underscore (standard MC rules)
for (wchar_t c : username)
{
if (!((c >= L'a' && c <= L'z') || (c >= L'A' && c <= L'Z') ||
(c >= L'0' && c <= L'9') || c == L'_'))
{
username = L"";
break;
}
}
}
void AuthResponsePacket::write(DataOutputStream *dos)
{
writeUtf(chosenScheme, dos);
writeUtf(mojangUuid, dos);
writeUtf(username, dos);
}
void AuthResponsePacket::handle(PacketListener *listener)
{
listener->handleAuthResponse(shared_from_this());
}
int AuthResponsePacket::getEstimatedSize()
{
return static_cast<int>(3 * sizeof(short) +
(chosenScheme.length() + mojangUuid.length() + username.length()) * sizeof(wchar_t));
}

View file

@ -0,0 +1,22 @@
#pragma once
#include "Packet.h"
using namespace std;
class AuthResponsePacket : public Packet, public enable_shared_from_this<AuthResponsePacket>
{
public:
wstring chosenScheme; // "mojang" or "offline"
wstring mojangUuid; // dashed format
wstring username; // chosen username
AuthResponsePacket();
AuthResponsePacket(const wstring& chosenScheme, const wstring& mojangUuid, const wstring& username);
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return make_shared<AuthResponsePacket>(); }
virtual int getId() { return 171; }
};

View file

@ -0,0 +1,76 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthResultPacket.h"
AuthResultPacket::AuthResultPacket()
{
success = false;
}
AuthResultPacket::AuthResultPacket(bool success, const wstring& assignedUuid, const wstring& assignedUsername,
const wstring& errorMessage, const wstring& skinKey,
std::vector<uint8_t> skinData)
{
this->success = success;
this->assignedUuid = assignedUuid;
this->assignedUsername = assignedUsername;
this->errorMessage = errorMessage;
this->skinKey = skinKey;
this->skinData = std::move(skinData);
}
void AuthResultPacket::read(DataInputStream *dis)
{
success = dis->readBoolean();
assignedUuid = readUtf(dis, 64);
assignedUsername = readUtf(dis, 64);
errorMessage = readUtf(dis, 256);
skinKey = readUtf(dis, 256);
// Read inline skin data (int length + raw bytes)
// Cap to 32KB — a valid 64x64 RGBA skin PNG is ~4KB compressed.
int skinSize = dis->readInt();
if (skinSize > 0 && skinSize <= 32768)
{
skinData.resize(static_cast<size_t>(skinSize));
for (int i = 0; i < skinSize; i++)
skinData[i] = dis->readByte();
}
else
{
skinData.clear();
// Consume declared bytes to keep stream synchronized (same fix as readUtf)
if (skinSize > 0)
{
for (int i = 0; i < skinSize; i++)
dis->readByte();
}
}
}
void AuthResultPacket::write(DataOutputStream *dos)
{
dos->writeBoolean(success);
writeUtf(assignedUuid, dos);
writeUtf(assignedUsername, dos);
writeUtf(errorMessage, dos);
writeUtf(skinKey, dos);
int skinSize = static_cast<int>(skinData.size());
dos->writeInt(skinSize);
for (int i = 0; i < skinSize; i++)
dos->writeByte(skinData[i]);
}
void AuthResultPacket::handle(PacketListener *listener)
{
listener->handleAuthResult(shared_from_this());
}
int AuthResultPacket::getEstimatedSize()
{
return static_cast<int>(sizeof(bool) + 4 * sizeof(short) +
(assignedUuid.length() + assignedUsername.length() + errorMessage.length() + skinKey.length()) * sizeof(wchar_t)
+ sizeof(int) + skinData.size());
}

View file

@ -0,0 +1,28 @@
#pragma once
#include "Packet.h"
#include <vector>
using namespace std;
class AuthResultPacket : public Packet, public enable_shared_from_this<AuthResultPacket>
{
public:
bool success;
wstring assignedUuid; // final UUID assigned by server (dashed)
wstring assignedUsername; // final username
wstring errorMessage; // empty if success
wstring skinKey; // memory-texture key (e.g. "mojang_skin_{uuid}.png")
std::vector<uint8_t> skinData; // raw PNG bytes of the Mojang skin (already cropped to 64x32)
AuthResultPacket();
AuthResultPacket(bool success, const wstring& assignedUuid, const wstring& assignedUsername,
const wstring& errorMessage, const wstring& skinKey = L"",
std::vector<uint8_t> skinData = {});
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return make_shared<AuthResultPacket>(); }
virtual int getId() { return 172; }
};

View file

@ -0,0 +1,56 @@
#include "stdafx.h"
#include "InputOutputStream.h"
#include "PacketListener.h"
#include "AuthSchemePacket.h"
AuthSchemePacket::AuthSchemePacket()
{
}
AuthSchemePacket::AuthSchemePacket(const vector<wstring>& schemes, const wstring& serverId)
{
this->schemes = schemes;
this->serverId = serverId;
}
void AuthSchemePacket::read(DataInputStream *dis)
{
int count = dis->readInt();
if (count < 0 || count > 16) count = 0; // sane upper bound for auth schemes
schemes.clear();
schemes.reserve(static_cast<size_t>(count));
for (int i = 0; i < count; i++)
{
wstring scheme = readUtf(dis, 32);
// Only accept known scheme names; discard unknown/empty
if (scheme == L"mojang" || scheme == L"offline" || scheme == L"elyby")
schemes.push_back(std::move(scheme));
}
serverId = readUtf(dis, 64);
}
void AuthSchemePacket::write(DataOutputStream *dos)
{
dos->writeInt(static_cast<int>(schemes.size()));
for (auto& s : schemes)
{
writeUtf(s, dos);
}
writeUtf(serverId, dos);
}
void AuthSchemePacket::handle(PacketListener *listener)
{
listener->handleAuthScheme(shared_from_this());
}
int AuthSchemePacket::getEstimatedSize()
{
int size = sizeof(int);
for (auto& s : schemes)
{
size += sizeof(short) + static_cast<int>(s.length()) * sizeof(wchar_t);
}
size += sizeof(short) + static_cast<int>(serverId.length()) * sizeof(wchar_t);
return size;
}

View file

@ -0,0 +1,22 @@
#pragma once
#include "Packet.h"
using namespace std;
class AuthSchemePacket : public Packet, public enable_shared_from_this<AuthSchemePacket>
{
public:
// Schemes: "mojang", "offline"
vector<wstring> schemes;
wstring serverId; // random hex challenge (20 chars), empty if offline-only
AuthSchemePacket();
AuthSchemePacket(const vector<wstring>& schemes, const wstring& serverId);
virtual void read(DataInputStream *dis);
virtual void write(DataOutputStream *dos);
virtual void handle(PacketListener *listener);
virtual int getEstimatedSize();
static shared_ptr<Packet> create() { return make_shared<AuthSchemePacket>(); }
virtual int getId() { return 170; }
};

View file

@ -189,18 +189,23 @@ bool Connection::writeTick()
return didSomething; return didSomething;
// try { // try {
if (!outgoing.empty() && (fakeLag == 0 || System::currentTimeMillis() - outgoing.front()->createTime >= fakeLag))
{ {
shared_ptr<Packet> packet; shared_ptr<Packet> packet;
bool hasPacket = false;
EnterCriticalSection(&writeLock); EnterCriticalSection(&writeLock);
if (!outgoing.empty() && (fakeLag == 0 || System::currentTimeMillis() - outgoing.front()->createTime >= fakeLag))
packet = outgoing.front(); {
outgoing.pop(); packet = outgoing.front();
estimatedRemaining -= packet->getEstimatedSize() + 1; outgoing.pop();
estimatedRemaining -= packet->getEstimatedSize() + 1;
hasPacket = true;
}
LeaveCriticalSection(&writeLock); LeaveCriticalSection(&writeLock);
if (hasPacket)
{
Packet::writePacket(packet, bufferedDos); Packet::writePacket(packet, bufferedDos);
@ -231,24 +236,30 @@ bool Connection::writeTick()
writeSizes[packet->getId()] += packet->getEstimatedSize() + 1; writeSizes[packet->getId()] += packet->getEstimatedSize() + 1;
didSomething = true; didSomething = true;
} }
}
if ((slowWriteDelay-- <= 0) && !outgoing_slow.empty() && (fakeLag == 0 || System::currentTimeMillis() - outgoing_slow.front()->createTime >= fakeLag))
{ {
shared_ptr<Packet> packet; shared_ptr<Packet> slowPacket;
bool hasSlowPacket = false;
//synchronized (writeLock) {
EnterCriticalSection(&writeLock); EnterCriticalSection(&writeLock);
bool slowReady = (slowWriteDelay <= 0) && !outgoing_slow.empty()
packet = outgoing_slow.front(); && (fakeLag == 0 || System::currentTimeMillis() - outgoing_slow.front()->createTime >= fakeLag);
outgoing_slow.pop(); if (slowReady)
estimatedRemaining -= packet->getEstimatedSize() + 1; {
slowPacket = outgoing_slow.front();
outgoing_slow.pop();
estimatedRemaining -= slowPacket->getEstimatedSize() + 1;
hasSlowPacket = true;
}
LeaveCriticalSection(&writeLock); LeaveCriticalSection(&writeLock);
if (slowWriteDelay > 0) slowWriteDelay--; // decrement only when positive, matching original (slowWriteDelay-- <= 0)
if (hasSlowPacket)
{
// If the shouldDelay flag is still set at this point then we want to write it to QNet as a single packet with priority flags // If the shouldDelay flag is still set at this point then we want to write it to QNet as a single packet with priority flags
// Otherwise just buffer the packet with other outgoing packets as the java game did // Otherwise just buffer the packet with other outgoing packets as the java game did
if(packet->shouldDelay) if(slowPacket->shouldDelay)
{ {
// Flush any buffered data BEFORE writing directly to the socket. // Flush any buffered data BEFORE writing directly to the socket.
// bufferedDos and sos->writeWithFlags both write to the same underlying // bufferedDos and sos->writeWithFlags both write to the same underlying
@ -258,7 +269,7 @@ bool Connection::writeTick()
// the TCP stream on the receiving end. // the TCP stream on the receiving end.
bufferedDos->flush(); bufferedDos->flush();
Packet::writePacket(packet, byteArrayDos); Packet::writePacket(slowPacket, byteArrayDos);
// 4J Stu - Changed this so that rather than writing to the network stream through a buffered stream we want to: // 4J Stu - Changed this so that rather than writing to the network stream through a buffered stream we want to:
// a) Only push whole "game" packets to QNet, rather than amalgamated chunks of data that may include many packets, and partial packets // a) Only push whole "game" packets to QNet, rather than amalgamated chunks of data that may include many packets, and partial packets
@ -273,7 +284,7 @@ bool Connection::writeTick()
} }
else else
{ {
Packet::writePacket(packet, bufferedDos); Packet::writePacket(slowPacket, bufferedDos);
} }
#ifndef _CONTENT_PACKAGE #ifndef _CONTENT_PACKAGE
@ -281,26 +292,24 @@ bool Connection::writeTick()
if( !socket->isLocal() ) if( !socket->isLocal() )
{ {
int playerId = 0; int playerId = 0;
if( !socket->isLocal() ) Socket *socket = getSocket();
if( socket )
{ {
Socket *socket = getSocket(); INetworkPlayer *player = socket->getPlayer();
if( socket ) if( player )
{ {
INetworkPlayer *player = socket->getPlayer(); playerId = player->GetSmallId();
if( player )
{
playerId = player->GetSmallId();
}
} }
Packet::recordOutgoingPacket(packet,playerId);
} }
Packet::recordOutgoingPacket(slowPacket,playerId);
} }
#endif #endif
writeSizes[packet->getId()] += packet->getEstimatedSize() + 1; writeSizes[slowPacket->getId()] += slowPacket->getEstimatedSize() + 1;
slowWriteDelay = 0; slowWriteDelay = 0;
didSomething = true; didSomething = true;
} }
}
/* 4J JEV, removed try/catch /* 4J JEV, removed try/catch
} catch (Exception e) { } catch (Exception e) {
if (!disconnected) handleException(e); if (!disconnected) handleException(e);
@ -384,22 +393,9 @@ void Connection::close(DisconnectPacket::eDisconnectReason reason, ...)
disconnectReason = reason;//va_arg( input, const wstring ); disconnectReason = reason;//va_arg( input, const wstring );
vector<void *> objs = vector<void *>(); // unused, clear for safety
void *i = nullptr; disconnectReasonObjects = nullptr;
while (i != nullptr) va_end(input);
{
i = va_arg( input, void* );
objs.push_back(i);
}
if( objs.size() )
{
disconnectReasonObjects = &objs[0];
}
else
{
disconnectReasonObjects = nullptr;
}
// int count = 0, sum = 0, i = first; // int count = 0, sum = 0, i = first;
// va_list marker; // va_list marker;

View file

@ -17,6 +17,6 @@ public:
virtual short readShort() = 0; virtual short readShort() = 0;
virtual wchar_t readChar() = 0; virtual wchar_t readChar() = 0;
virtual wstring readUTF() = 0; virtual wstring readUTF() = 0;
virtual PlayerUID readPlayerUID() = 0; // 4J Added virtual PlayerUID readPlayerUID() = 0;
virtual int skipBytes(int n) = 0; virtual int skipBytes(int n) = 0;
}; };

View file

@ -83,7 +83,9 @@ void DataInputStream::close()
//the boolean value read. //the boolean value read.
bool DataInputStream::readBoolean() bool DataInputStream::readBoolean()
{ {
return stream->read() != 0; int val = stream->read();
if (val == -1) return false;
return val != 0;
} }
//Reads and returns one input byte. The byte is treated as a signed value in the range -128 through 127, inclusive. //Reads and returns one input byte. The byte is treated as a signed value in the range -128 through 127, inclusive.
@ -92,12 +94,16 @@ bool DataInputStream::readBoolean()
//the 8-bit value read. //the 8-bit value read.
byte DataInputStream::readByte() byte DataInputStream::readByte()
{ {
return static_cast<byte>(stream->read()); int val = stream->read();
if (val == -1) return 0;
return static_cast<byte>(val);
} }
unsigned char DataInputStream::readUnsignedByte() unsigned char DataInputStream::readUnsignedByte()
{ {
return static_cast<unsigned char>(stream->read()); int val = stream->read();
if (val == -1) return 0;
return static_cast<unsigned char>(val);
} }
//Reads two input bytes and returns a char value. Let a be the first byte read and b be the second byte. The value returned is: //Reads two input bytes and returns a char value. Let a be the first byte read and b be the second byte. The value returned is:
@ -110,6 +116,7 @@ wchar_t DataInputStream::readChar()
{ {
int a = stream->read(); int a = stream->read();
int b = stream->read(); int b = stream->read();
if (a == -1 || b == -1) return 0;
return static_cast<wchar_t>((a << 8) | (b & 0xff)); return static_cast<wchar_t>((a << 8) | (b & 0xff));
} }
@ -201,6 +208,7 @@ int DataInputStream::readInt()
int b = stream->read(); int b = stream->read();
int c = stream->read(); int c = stream->read();
int d = stream->read(); int d = stream->read();
if (a == -1 || b == -1 || c == -1 || d == -1) return 0;
int bits = (((a & 0xff) << 24) | ((b & 0xff) << 16) | int bits = (((a & 0xff) << 24) | ((b & 0xff) << 16) |
((c & 0xff) << 8) | (d & 0xff)); ((c & 0xff) << 8) | (d & 0xff));
return bits; return bits;
@ -231,15 +239,17 @@ int64_t DataInputStream::readLong()
int64_t f = stream->read(); int64_t f = stream->read();
int64_t g = stream->read(); int64_t g = stream->read();
int64_t h = stream->read(); int64_t h = stream->read();
if (a == -1 || b == -1 || c == -1 || d == -1 ||
e == -1 || f == -1 || g == -1 || h == -1) return 0;
int64_t bits = (((a & 0xff) << 56) | int64_t bits = (((a & 0xffLL) << 56) |
((b & 0xff) << 48) | ((b & 0xffLL) << 48) |
((c & 0xff) << 40) | ((c & 0xffLL) << 40) |
((d & 0xff) << 32) | ((d & 0xffLL) << 32) |
((e & 0xff) << 24) | ((e & 0xffLL) << 24) |
((f & 0xff) << 16) | ((f & 0xffLL) << 16) |
((g & 0xff) << 8) | ((g & 0xffLL) << 8) |
((h & 0xff))); ((h & 0xffLL)));
return bits; return bits;
} }
@ -254,6 +264,7 @@ short DataInputStream::readShort()
{ {
int a = stream->read(); int a = stream->read();
int b = stream->read(); int b = stream->read();
if (a == -1 || b == -1) return 0;
return static_cast<short>((a << 8) | (b & 0xff)); return static_cast<short>((a << 8) | (b & 0xff));
} }
@ -261,6 +272,7 @@ unsigned short DataInputStream::readUnsignedShort()
{ {
int a = stream->read(); int a = stream->read();
int b = stream->read(); int b = stream->read();
if (a == -1 || b == -1) return 0;
return static_cast<unsigned short>((a << 8) | (b & 0xff)); return static_cast<unsigned short>((a << 8) | (b & 0xff));
} }
@ -519,19 +531,12 @@ int DataInputStream::readUTFChar()
return returnValue; return returnValue;
} }
// 4J Added
PlayerUID DataInputStream::readPlayerUID() PlayerUID DataInputStream::readPlayerUID()
{ {
PlayerUID returnValue; PlayerUID uid;
#if defined(__PS3__) || defined(__ORBIS__) || defined(__PSVITA__) uid.hi = readLong();
for(int idPos=0;idPos<sizeof(PlayerUID); idPos++) uid.lo = readLong();
((char*)&returnValue)[idPos] = readByte(); return uid;
#elif defined(_DURANGO)
returnValue = readUTF();
#else
returnValue = readLong();
#endif // PS3
return returnValue;
} }
void DataInputStream::deleteChildStream() void DataInputStream::deleteChildStream()

View file

@ -30,7 +30,7 @@ public:
virtual wstring readUTF(); virtual wstring readUTF();
void deleteChildStream(); void deleteChildStream();
virtual int readUTFChar(); virtual int readUTFChar();
virtual PlayerUID readPlayerUID(); // 4J Added virtual PlayerUID readPlayerUID();
virtual int64_t skip(int64_t n); virtual int64_t skip(int64_t n);
virtual int skipBytes(int n); virtual int skipBytes(int n);
}; };

View file

@ -16,5 +16,5 @@ public:
virtual void writeChar(wchar_t v) = 0; virtual void writeChar(wchar_t v) = 0;
virtual void writeChars(const wstring& s) = 0; virtual void writeChars(const wstring& s) = 0;
virtual void writeUTF(const wstring& a) = 0; virtual void writeUTF(const wstring& a) = 0;
virtual void writePlayerUID(PlayerUID player) = 0; // 4J Added virtual void writePlayerUID(PlayerUID player) = 0;
}; };

View file

@ -69,6 +69,7 @@ void DataOutputStream::close()
void DataOutputStream::writeByte(byte a) void DataOutputStream::writeByte(byte a)
{ {
stream->write( a ); stream->write( a );
written += 1;
} }
//Converts the double argument to a long using the doubleToLongBits method in class Double, //Converts the double argument to a long using the doubleToLongBits method in class Double,
@ -81,8 +82,6 @@ void DataOutputStream::writeDouble(double a)
int64_t bits = Double::doubleToLongBits( a ); int64_t bits = Double::doubleToLongBits( a );
writeLong( bits ); writeLong( bits );
// TODO 4J Stu - Error handling?
written += 8;
} }
//Converts the float argument to an int using the floatToIntBits method in class Float, //Converts the float argument to an int using the floatToIntBits method in class Float,
@ -95,8 +94,6 @@ void DataOutputStream::writeFloat(float a)
int bits = Float::floatToIntBits( a ); int bits = Float::floatToIntBits( a );
writeInt( bits ); writeInt( bits );
// TODO 4J Stu - Error handling?
written += 4;
} }
//Writes an int to the underlying output stream as four bytes, high byte first. If no exception is thrown, the counter written is incremented by 4. //Writes an int to the underlying output stream as four bytes, high byte first. If no exception is thrown, the counter written is incremented by 4.
@ -127,7 +124,7 @@ void DataOutputStream::writeLong(int64_t a)
stream->write( (a >> 8) & 0xff ); stream->write( (a >> 8) & 0xff );
stream->write( a & 0xff ); stream->write( a & 0xff );
// TODO 4J Stu - Error handling? // TODO 4J Stu - Error handling?
written += 4; written += 8;
} }
//Writes a short to the underlying output stream as two bytes, high byte first. //Writes a short to the underlying output stream as two bytes, high byte first.
@ -262,15 +259,8 @@ void DataOutputStream::writeUTF(const wstring& str)
delete[] bytearr.data; delete[] bytearr.data;
} }
// 4J Added
void DataOutputStream::writePlayerUID(PlayerUID player) void DataOutputStream::writePlayerUID(PlayerUID player)
{ {
#if defined(__PS3__) || defined(__ORBIS__) || defined (__PSVITA__) writeLong(player.hi);
for(int idPos=0;idPos<sizeof(PlayerUID); idPos++) writeLong(player.lo);
writeByte(((char*)&player)[idPos]);
#elif defined(_DURANGO)
writeUTF(player.toString());
#else
writeLong(player);
#endif // PS3
} }

View file

@ -35,6 +35,7 @@ enum EDefaultSkins
eDefaultSkins_Skin5, eDefaultSkins_Skin5,
eDefaultSkins_Skin6, eDefaultSkins_Skin6,
eDefaultSkins_Skin7, eDefaultSkins_Skin7,
eDefaultSkins_MojangSkin,
eDefaultSkins_Count, eDefaultSkins_Count,
}; };

View file

@ -57,7 +57,7 @@ void _MapDataMappings::setMapping(int id, PlayerUID xuid, int dimension)
const int offset = (2*(id%4)); const int offset = (2*(id%4));
// Reset it first // Reset it first
dimensions[id>>2] &= ~( 2 << offset ); dimensions[id>>2] &= ~( 3 << offset );
switch(dimension) switch(dimension)
{ {
case 0: // Overworld case 0: // Overworld
@ -78,11 +78,11 @@ void _MapDataMappings::setMapping(int id, PlayerUID xuid, int dimension)
} }
} }
// Old version the only used 1 bit for dimension indexing // Old version that only used 1 bit for dimension indexing (legacy 64-bit XUIDs)
_MapDataMappings_old::_MapDataMappings_old() _MapDataMappings_old::_MapDataMappings_old()
{ {
#ifndef _DURANGO #ifndef _DURANGO
ZeroMemory(xuids,sizeof(PlayerUID)*MAXIMUM_MAP_SAVE_DATA); ZeroMemory(xuids,sizeof(uint64_t)*MAXIMUM_MAP_SAVE_DATA);
#endif #endif
ZeroMemory(dimensions,sizeof(byte)*(MAXIMUM_MAP_SAVE_DATA/8)); ZeroMemory(dimensions,sizeof(byte)*(MAXIMUM_MAP_SAVE_DATA/8));
} }
@ -92,7 +92,7 @@ int _MapDataMappings_old::getDimension(int id)
return dimensions[id>>3] & (128 >> (id%8) ) ? -1 : 0; return dimensions[id>>3] & (128 >> (id%8) ) ? -1 : 0;
} }
void _MapDataMappings_old::setMapping(int id, PlayerUID xuid, int dimension) void _MapDataMappings_old::setMapping(int id, uint64_t xuid, int dimension)
{ {
xuids[id] = xuid; xuids[id] = xuid;
if( dimension == 0 ) if( dimension == 0 )
@ -140,7 +140,6 @@ void DirectoryLevelStorage::PlayerMappings::writeMappings(DataOutputStream *dos)
dos->writeInt(m_mappings.size()); dos->writeInt(m_mappings.size());
for (const auto& it : m_mappings ) for (const auto& it : m_mappings )
{ {
app.DebugPrintf(" -- %lld (0x%016llx) = %d\n", it.first, it.first, it.second);
dos->writeLong(it.first); dos->writeLong(it.first);
dos->writeInt(it.second); dos->writeInt(it.second);
} }
@ -149,12 +148,12 @@ void DirectoryLevelStorage::PlayerMappings::writeMappings(DataOutputStream *dos)
void DirectoryLevelStorage::PlayerMappings::readMappings(DataInputStream *dis) void DirectoryLevelStorage::PlayerMappings::readMappings(DataInputStream *dis)
{ {
const int count = dis->readInt(); const int count = dis->readInt();
for(unsigned int i = 0; i < count; ++i) const unsigned int safeCount = (count < 0 || count > 10000) ? 0 : static_cast<unsigned int>(count);
for(unsigned int i = 0; i < safeCount; ++i)
{ {
int64_t index = dis->readLong(); int64_t index = dis->readLong();
const int id = dis->readInt(); const int id = dis->readInt();
m_mappings[index] = id; m_mappings[index] = id;
app.DebugPrintf(" -- %lld (0x%016llx) = %d\n", index, index, id);
} }
} }
#endif #endif
@ -280,15 +279,10 @@ LevelData *DirectoryLevelStorage::prepareLevel()
ByteArrayInputStream bais(data); ByteArrayInputStream bais(data);
DataInputStream dis(&bais); DataInputStream dis(&bais);
const int count = dis.readInt(); const int count = dis.readInt();
app.DebugPrintf("Loading %d mappings\n", count); const unsigned int safeCount = (count < 0 || count > 10000) ? 0 : static_cast<unsigned int>(count);
for(unsigned int i = 0; i < count; ++i) for(unsigned int i = 0; i < safeCount; ++i)
{ {
PlayerUID playerUid = dis.readPlayerUID(); PlayerUID playerUid = dis.readPlayerUID();
#ifdef _WINDOWS64
app.DebugPrintf(" -- %d\n", playerUid);
#else
app.DebugPrintf(" -- %ls\n", playerUid.toString().c_str());
#endif
m_playerMappings[playerUid].readMappings(&dis); m_playerMappings[playerUid].readMappings(&dis);
} }
dis.readFully(m_usedMappings); dis.readFully(m_usedMappings);
@ -296,7 +290,9 @@ LevelData *DirectoryLevelStorage::prepareLevel()
if(getSaveFile()->getSaveVersion() < END_DIMENSION_MAP_MAPPINGS_SAVE_VERSION) if(getSaveFile()->getSaveVersion() < END_DIMENSION_MAP_MAPPINGS_SAVE_VERSION)
{ {
// Very old format: 64-bit XUIDs + 1-bit dimension indexing
MapDataMappings_old oldMapDataMappings; MapDataMappings_old oldMapDataMappings;
ZeroMemory(&oldMapDataMappings, sizeof(oldMapDataMappings));
getSaveFile()->readFile( fileEntry, getSaveFile()->readFile( fileEntry,
&oldMapDataMappings, // data buffer &oldMapDataMappings, // data buffer
sizeof(MapDataMappings_old), // number of bytes to read sizeof(MapDataMappings_old), // number of bytes to read
@ -306,17 +302,58 @@ LevelData *DirectoryLevelStorage::prepareLevel()
for(unsigned int i = 0; i < MAXIMUM_MAP_SAVE_DATA; ++i) for(unsigned int i = 0; i < MAXIMUM_MAP_SAVE_DATA; ++i)
{ {
m_saveableMapDataMappings.setMapping(i,oldMapDataMappings.xuids[i],oldMapDataMappings.getDimension(i)); // Migrate 64-bit XUID to 128-bit UUID: put old value in hi, lo=0
GameUUID migrated;
migrated.hi = oldMapDataMappings.xuids[i];
migrated.lo = 0;
m_saveableMapDataMappings.setMapping(i, migrated, oldMapDataMappings.getDimension(i));
} }
} }
else else
{ {
getSaveFile()->readFile( fileEntry, // Try to detect whether this is old 64-bit or new 128-bit format
&m_saveableMapDataMappings, // data buffer // by comparing file size against expected struct sizes
sizeof(MapDataMappings), // number of bytes to read DWORD fileSize = fileEntry->getFileSize();
&NumberOfBytesRead // number of bytes read DWORD expectedNew = sizeof(MapDataMappings);
); DWORD expectedLegacy64 = sizeof(MapDataMappings_legacy64);
assert( NumberOfBytesRead == sizeof(MapDataMappings) );
if (fileSize == expectedNew)
{
// New 128-bit format
getSaveFile()->readFile( fileEntry,
&m_saveableMapDataMappings,
sizeof(MapDataMappings),
&NumberOfBytesRead
);
assert( NumberOfBytesRead == sizeof(MapDataMappings) );
}
else if (fileSize == expectedLegacy64 || fileSize > 0)
{
// Legacy 64-bit format — migrate
MapDataMappings_legacy64 legacy;
ZeroMemory(&legacy, sizeof(legacy));
getSaveFile()->readFile( fileEntry,
&legacy,
(fileSize < sizeof(legacy)) ? fileSize : sizeof(legacy),
&NumberOfBytesRead
);
for(unsigned int i = 0; i < MAXIMUM_MAP_SAVE_DATA; ++i)
{
GameUUID migrated;
migrated.hi = legacy.xuids[i];
migrated.lo = 0;
// Decode raw 2-bit value to Minecraft dimension ID
int rawDim = (legacy.dimensions[i>>2] >> (2*(i%4))) & 3;
int dimension = 0;
switch (rawDim) {
case 1: dimension = -1; break; // Nether
case 2: dimension = 1; break; // End
default: break;
}
m_saveableMapDataMappings.setMapping(i, migrated, dimension);
}
}
} }
memcpy(&m_mapDataMappings,&m_saveableMapDataMappings,sizeof(MapDataMappings)); memcpy(&m_mapDataMappings,&m_saveableMapDataMappings,sizeof(MapDataMappings));
@ -398,7 +435,7 @@ void DirectoryLevelStorage::save(shared_ptr<Player> player)
#elif defined(_DURANGO) #elif defined(_DURANGO)
ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getXuid().toString() + L".dat" ); ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getXuid().toString() + L".dat" );
#else #else
const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + std::to_wstring( player->getXuid() ) + L".dat" ); const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + player->getXuid().toWDashed() + L".dat" );
#endif #endif
// If saves are disabled (e.g. because we are writing the save buffer to disk) then cache this player data // If saves are disabled (e.g. because we are writing the save buffer to disk) then cache this player data
if(StorageManager.GetSaveDisabled()) if(StorageManager.GetSaveDisabled())
@ -447,7 +484,7 @@ CompoundTag *DirectoryLevelStorage::loadPlayerDataTag(PlayerUID xuid)
#elif defined(_DURANGO) #elif defined(_DURANGO)
ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + xuid.toString() + L".dat" ); ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + xuid.toString() + L".dat" );
#else #else
const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + std::to_wstring( xuid ) + L".dat" ); const ConsoleSavePath realFile = ConsoleSavePath( playerDir.getName() + xuid.toWDashed() + L".dat" );
#endif #endif
const auto it = m_cachedSaveData.find(realFile.getName()); const auto it = m_cachedSaveData.find(realFile.getName());
if(it != m_cachedSaveData.end() ) if(it != m_cachedSaveData.end() )
@ -640,6 +677,28 @@ int DirectoryLevelStorage::getAuxValueForMap(PlayerUID xuid, int dimension, int
mapId = i; mapId = i;
} }
} }
// Fallback: match migrated entries where lo==0 (old 64-bit XUID stored as {xuid, 0})
// and update them in-place to the player's full new UUID.
if (!foundMapping)
{
for(unsigned int i = 0; i < MAXIMUM_MAP_SAVE_DATA; ++i)
{
if(m_mapDataMappings.xuids[i].hi == xuid.hi &&
m_mapDataMappings.xuids[i].lo == 0 &&
xuid.lo != 0 &&
m_mapDataMappings.getDimension(i) == dimension)
{
// Update the migrated entry to the full UUID
m_mapDataMappings.setMapping(i, xuid, dimension);
m_saveableMapDataMappings.setMapping(i, xuid, dimension);
foundMapping = true;
mapId = i;
break;
}
}
}
if( !foundMapping && mapId >= 0 && mapId < MAXIMUM_MAP_SAVE_DATA ) if( !foundMapping && mapId >= 0 && mapId < MAXIMUM_MAP_SAVE_DATA )
{ {
m_mapDataMappings.setMapping(mapId, xuid, dimension); m_mapDataMappings.setMapping(mapId, xuid, dimension);
@ -681,14 +740,8 @@ void DirectoryLevelStorage::saveMapIdLookup()
ByteArrayOutputStream baos; ByteArrayOutputStream baos;
DataOutputStream dos(&baos); DataOutputStream dos(&baos);
dos.writeInt(m_playerMappings.size()); dos.writeInt(m_playerMappings.size());
app.DebugPrintf("Saving %d mappings\n", m_playerMappings.size());
for ( auto& it : m_playerMappings ) for ( auto& it : m_playerMappings )
{ {
#ifdef _WINDOWS64
app.DebugPrintf(" -- %d\n", it.first);
#else
app.DebugPrintf(" -- %ls\n", it.first.toString().c_str());
#endif
dos.writePlayerUID(it.first); dos.writePlayerUID(it.first);
it.second.writeMappings(&dos); it.second.writeMappings(&dos);
} }

Some files were not shown because too many files have changed in this diff Show more