mirror of
https://github.com/smartcmd/MinecraftConsoles.git
synced 2026-08-20 09:57:09 +00:00
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:
parent
a94ee1ca22
commit
4f2352361a
|
|
@ -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
40
MCAuth/CMakeLists.txt
Normal 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
156
MCAuth/MCAuth.vcxproj
Normal 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
230
MCAuth/include/MCAuth.h
Normal 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
|
||||||
211
MCAuth/include/MCAuthManager.h
Normal file
211
MCAuth/include/MCAuthManager.h
Normal 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
398
MCAuth/src/MCAuthCrypto.cpp
Normal 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
70
MCAuth/src/MCAuthCrypto.h
Normal 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
169
MCAuth/src/MCAuthElyby.cpp
Normal 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
221
MCAuth/src/MCAuthHttp.cpp
Normal 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
22
MCAuth/src/MCAuthHttp.h
Normal 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
205
MCAuth/src/MCAuthInternal.h
Normal 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
453
MCAuth/src/MCAuthJava.cpp
Normal 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
|
||||||
986
MCAuth/src/MCAuthManager.cpp
Normal file
986
MCAuth/src/MCAuthManager.cpp
Normal 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;
|
||||||
|
}
|
||||||
476
MCAuth/src/MCAuthSession.cpp
Normal file
476
MCAuth/src/MCAuthSession.cpp
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
BIN
Minecraft.Client/Common/Media/platformskin.swf
Normal file
BIN
Minecraft.Client/Common/Media/platformskin.swf
Normal file
Binary file not shown.
BIN
Minecraft.Client/Common/Media/platformskinHD.swf
Normal file
BIN
Minecraft.Client/Common/Media/platformskinHD.swf
Normal file
Binary file not shown.
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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__
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2030
Minecraft.Client/Common/UI/NativeUIRenderer.cpp
Normal file
2030
Minecraft.Client/Common/UI/NativeUIRenderer.cpp
Normal file
File diff suppressed because it is too large
Load diff
346
Minecraft.Client/Common/UI/NativeUIRenderer.h
Normal file
346
Minecraft.Client/Common/UI/NativeUIRenderer.h
Normal 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
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
1712
Minecraft.Client/Common/UI/UIScene_MSAuth.cpp
Normal file
1712
Minecraft.Client/Common/UI/UIScene_MSAuth.cpp
Normal file
File diff suppressed because it is too large
Load diff
158
Minecraft.Client/Common/UI/UIScene_MSAuth.h
Normal file
158
Minecraft.Client/Common/UI/UIScene_MSAuth.h
Normal 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
|
||||||
|
};
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>', '$');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
42
Minecraft.Client/Windows64/Windows64_Uuid.h
Normal file
42
Minecraft.Client/Windows64/Windows64_Uuid.h
Normal 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
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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 )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
54
Minecraft.World/AuthResponsePacket.cpp
Normal file
54
Minecraft.World/AuthResponsePacket.cpp
Normal 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));
|
||||||
|
}
|
||||||
22
Minecraft.World/AuthResponsePacket.h
Normal file
22
Minecraft.World/AuthResponsePacket.h
Normal 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; }
|
||||||
|
};
|
||||||
76
Minecraft.World/AuthResultPacket.cpp
Normal file
76
Minecraft.World/AuthResultPacket.cpp
Normal 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());
|
||||||
|
}
|
||||||
28
Minecraft.World/AuthResultPacket.h
Normal file
28
Minecraft.World/AuthResultPacket.h
Normal 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; }
|
||||||
|
};
|
||||||
56
Minecraft.World/AuthSchemePacket.cpp
Normal file
56
Minecraft.World/AuthSchemePacket.cpp
Normal 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;
|
||||||
|
}
|
||||||
22
Minecraft.World/AuthSchemePacket.h
Normal file
22
Minecraft.World/AuthSchemePacket.h
Normal 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; }
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
@ -35,6 +35,7 @@ enum EDefaultSkins
|
||||||
eDefaultSkins_Skin5,
|
eDefaultSkins_Skin5,
|
||||||
eDefaultSkins_Skin6,
|
eDefaultSkins_Skin6,
|
||||||
eDefaultSkins_Skin7,
|
eDefaultSkins_Skin7,
|
||||||
|
eDefaultSkins_MojangSkin,
|
||||||
|
|
||||||
eDefaultSkins_Count,
|
eDefaultSkins_Count,
|
||||||
};
|
};
|
||||||
|
|
@ -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
Loading…
Reference in a new issue