diff --git a/CMakeLists.txt b/CMakeLists.txt index 63071a0ed7..fc8c45389b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,68 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) +function(configure_msvc_system_includes) + if(NOT MSVC) + return() + endif() + + get_filename_component(_msvc_bin_dir "${CMAKE_CXX_COMPILER}" DIRECTORY) + get_filename_component(_msvc_root "${_msvc_bin_dir}/../../.." ABSOLUTE) + set(_msvc_include_dir "${_msvc_root}/include") + + set(_sdk_root "C:/Program Files (x86)/Windows Kits/10/Include") + set(_sdk_lib_root "C:/Program Files (x86)/Windows Kits/10/Lib") + set(_sdk_include_dirs) + set(_sdk_lib_dirs) + if(EXISTS "${_sdk_root}") + file(GLOB _sdk_versions LIST_DIRECTORIES true "${_sdk_root}/*") + if(_sdk_versions) + list(SORT _sdk_versions COMPARE NATURAL ORDER DESCENDING) + list(GET _sdk_versions 0 _sdk_version_dir) + foreach(_sdk_subdir ucrt shared um winrt cppwinrt) + if(EXISTS "${_sdk_version_dir}/${_sdk_subdir}") + list(APPEND _sdk_include_dirs "${_sdk_version_dir}/${_sdk_subdir}") + endif() + endforeach() + endif() + endif() + + if(EXISTS "${_sdk_lib_root}") + file(GLOB _sdk_lib_versions LIST_DIRECTORIES true "${_sdk_lib_root}/*") + if(_sdk_lib_versions) + list(SORT _sdk_lib_versions COMPARE NATURAL ORDER DESCENDING) + list(GET _sdk_lib_versions 0 _sdk_lib_version_dir) + foreach(_sdk_lib_subdir ucrt um) + if(EXISTS "${_sdk_lib_version_dir}/${_sdk_lib_subdir}/x64") + list(APPEND _sdk_lib_dirs "${_sdk_lib_version_dir}/${_sdk_lib_subdir}/x64") + endif() + endforeach() + endif() + endif() + + set(_system_include_dirs) + if(EXISTS "${_msvc_include_dir}") + list(APPEND _system_include_dirs "${_msvc_include_dir}") + endif() + list(APPEND _system_include_dirs ${_sdk_include_dirs}) + + set(_system_lib_dirs) + if(EXISTS "${_msvc_root}/lib/x64") + list(APPEND _system_lib_dirs "${_msvc_root}/lib/x64") + endif() + list(APPEND _system_lib_dirs ${_sdk_lib_dirs}) + + if(_system_include_dirs) + list(REMOVE_DUPLICATES _system_include_dirs) + include_directories(${_system_include_dirs}) + endif() + + if(_system_lib_dirs) + list(REMOVE_DUPLICATES _system_lib_dirs) + link_directories(${_system_lib_dirs}) + endif() +endfunction() + if(NOT WIN32) message(FATAL_ERROR "This CMake build currently supports Windows only.") endif() @@ -13,6 +75,8 @@ if(NOT CMAKE_SIZEOF_VOID_P EQUAL 8) message(FATAL_ERROR "Use a 64-bit generator/toolchain (x64).") endif() +configure_msvc_system_includes() + set(CMAKE_CONFIGURATION_TYPES "Debug" "Release" @@ -77,6 +141,7 @@ list(APPEND MINECRAFT_SHARED_DEFINES ${PLATFORM_DEFINES}) # Sources # --- add_subdirectory(Minecraft.World) +add_subdirectory(newauth) add_subdirectory(Minecraft.Client) if(PLATFORM_NAME STREQUAL "Windows64") # Server is only supported on Windows for now add_subdirectory(Minecraft.Server) diff --git a/Minecraft.Client/CMakeLists.txt b/Minecraft.Client/CMakeLists.txt index 9f75efd219..6693a7abb7 100644 --- a/Minecraft.Client/CMakeLists.txt +++ b/Minecraft.Client/CMakeLists.txt @@ -51,6 +51,7 @@ set_target_properties(Minecraft.Client PROPERTIES target_link_libraries(Minecraft.Client PRIVATE Minecraft.World + newauth d3d11 d3dcompiler XInput9_1_0 diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index a80af5d2c9..8962ef9113 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -58,6 +58,12 @@ #ifdef _WINDOWS64 #include "Xbox\Network\NetworkPlayerXbox.h" #include "Common\Network\PlatformNetworkManagerStub.h" +#include "..\newauth\include\newauth.h" +#include "..\newauth\include\newauthManager.h" +#include "..\Minecraft.World\AuthSchemePacket.h" +#include "..\Minecraft.World\AuthResponsePacket.h" +#include "..\Minecraft.World\AuthResultPacket.h" +#include "..\Minecraft.World\GameUUID.h" #endif @@ -368,6 +374,14 @@ void ClientConnection::handleLogin(shared_ptr packet) minecraft->player->setCustomSkin( app.GetPlayerSkinId(m_userIndex) ); minecraft->player->setCustomCape( app.GetPlayerCapeId(m_userIndex) ); +#ifdef _WINDOWS64 + // gives me a skin. + if (!m_authSkinKey.empty() && app.IsFileInMemoryTextures(m_authSkinKey)) + { + minecraft->player->customTextureUrl = m_authSkinKey; + app.DebugPrintf("Auth: Applied MS skin '%ls' to local player\n", m_authSkinKey.c_str()); + } +#endif minecraft->createPrimaryLocalPlayer(ProfileManager.GetPrimaryPad()); @@ -751,6 +765,45 @@ void ClientConnection::handleAddEntity(shared_ptr packet) } } +#ifdef _WINDOWS64 +bool ClientConnection::ensureRemoteNetworkPlayer(BYTE smallId, const std::wstring *playerName, PlayerUID xuid) +{ + if (smallId >= MINECRAFT_NET_MAX_PLAYERS) + return false; + + extern CPlatformNetworkManagerStub* g_pPlatformNetworkManager; + if (g_pPlatformNetworkManager == nullptr) + return false; + + INetworkPlayer *networkPlayer = g_NetworkManager.GetPlayerBySmallId(smallId); + IQNetPlayer *qnetPlayer = nullptr; + if (networkPlayer != nullptr) + { + qnetPlayer = static_cast(networkPlayer)->GetQNetPlayer(); + } + + if (qnetPlayer == nullptr) + { + qnetPlayer = &IQNet::m_player[smallId]; + extern void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost, bool isLocal); + Win64_SetupRemoteQNetPlayer(qnetPlayer, smallId, false, false); + g_pPlatformNetworkManager->NotifyPlayerJoined(qnetPlayer); + } + + if (playerName != nullptr && !playerName->empty()) + { + wcsncpy_s(qnetPlayer->m_gamertag, 32, playerName->c_str(), _TRUNCATE); + } + + if (xuid != INVALID_XUID) + { + qnetPlayer->m_resolvedXuid = xuid; + } + + return true; +} +#endif + void ClientConnection::handleAddExperienceOrb(shared_ptr packet) { shared_ptr e = std::make_shared(level, packet->x / 32.0, packet->y / 32.0, packet->z / 32.0, packet->value); @@ -882,6 +935,15 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) #ifdef _WINDOWS64 { + BYTE pendingSmallId = 0xFF; + auto pendingInfo = m_pendingRemotePlayerSmallIds.find(packet->id); + if (pendingInfo != m_pendingRemotePlayerSmallIds.end()) + { + pendingSmallId = pendingInfo->second; + ensureRemoteNetworkPlayer(pendingSmallId, &packet->name, player->getXuid()); + m_pendingRemotePlayerSmallIds.erase(pendingInfo); + } + IQNetPlayer* matchedQNetPlayer = nullptr; PlayerUID pktXuid = player->getXuid(); const PlayerUID WIN64_XUID_BASE = (PlayerUID)0xe000d45248242f2e; @@ -897,6 +959,16 @@ void ClientConnection::handleAddPlayer(shared_ptr packet) } } + if (matchedQNetPlayer == nullptr && pendingSmallId != 0xFF) + { + INetworkPlayer* np = g_NetworkManager.GetPlayerBySmallId(pendingSmallId); + if (np != nullptr) + { + NetworkPlayerXbox* npx = (NetworkPlayerXbox*)np; + matchedQNetPlayer = npx->GetQNetPlayer(); + } + } + // Current Win64 path: identify QNet player by name and attach packet XUID. if (matchedQNetPlayer == nullptr) { @@ -1087,6 +1159,8 @@ void ClientConnection::handleRemoveEntity(shared_ptr packe { for (int i = 0; i < packet->ids.length; i++) { + m_pendingRemotePlayerSmallIds.erase(packet->ids[i]); + shared_ptr entity = getEntity(packet->ids[i]); if (entity != nullptr && entity->GetType() == eTYPE_PLAYER) { @@ -2004,8 +2078,15 @@ void ClientConnection::handlePreLogin(shared_ptr packet) // 4J - Check that we can play with all the players already in the game who have Friends-Only UGC set BOOL canPlay = TRUE; BOOL canPlayLocal = TRUE; +#ifdef _WINDOWS64 + // yes, this looks wrong, but on Win64 we don't have the API to check friends of friends, so just assume that there - + // - is at least one friend and that we are friends with the host. fuck xbl. we can remedy this with discord in the future or some shit -- mattsumi + BOOL isAtLeastOneFriend = TRUE; + BOOL isFriendsWithHost = TRUE; +#else BOOL isAtLeastOneFriend = g_NetworkManager.IsHost(); BOOL isFriendsWithHost = TRUE; +#endif BOOL cantPlayContentRestricted = FALSE; if(!g_NetworkManager.IsHost()) @@ -2443,8 +2524,14 @@ void ClientConnection::handlePreLogin(shared_ptr packet) } BOOL allAllowed, friendsAllowed; ProfileManager.AllowedPlayerCreatedContent(m_userIndex,true,&allAllowed,&friendsAllowed); + +#ifdef _WINDOWS64 + // Save ugcPlayersVersion for the deferred LoginPacket sent after auth handshake + m_preLoginUgcPlayersVersion = packet->m_ugcPlayersVersion; +#else send(std::make_shared(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))); +#endif if(!g_NetworkManager.IsHost() ) { @@ -3556,9 +3643,31 @@ bool ClientConnection::isServerPacketListener() void ClientConnection::handlePlayerInfo(shared_ptr packet) { - unsigned int startingPrivileges = app.GetPlayerPrivileges(packet->m_networkSmallId); + BYTE networkSmallId = static_cast(packet->m_networkSmallId); + unsigned int startingPrivileges = app.GetPlayerPrivileges(networkSmallId); - INetworkPlayer *networkPlayer = g_NetworkManager.GetPlayerBySmallId(packet->m_networkSmallId); + INetworkPlayer *networkPlayer = g_NetworkManager.GetPlayerBySmallId(networkSmallId); + +#ifdef _WINDOWS64 + if (networkPlayer == nullptr && packet->m_entityId >= 0) + { + shared_ptr entity = getEntity(packet->m_entityId); + if (entity != nullptr && entity->instanceof(eTYPE_PLAYER)) + { + shared_ptr player = dynamic_pointer_cast(entity); + if (player != nullptr) + { + std::wstring playerName = player->getName(); + ensureRemoteNetworkPlayer(networkSmallId, &playerName, player->getXuid()); + networkPlayer = g_NetworkManager.GetPlayerBySmallId(networkSmallId); + } + } + else + { + m_pendingRemotePlayerSmallIds[packet->m_entityId] = networkSmallId; + } + } +#endif if(networkPlayer != nullptr && networkPlayer->IsHost()) { @@ -3568,7 +3677,7 @@ void ClientConnection::handlePlayerInfo(shared_ptr packet) } // 4J Stu - Repurposed this packet for player info that we want - app.UpdatePlayerInfo(packet->m_networkSmallId, packet->m_playerColourIndex, packet->m_playerPrivileges); + app.UpdatePlayerInfo(networkSmallId, packet->m_playerColourIndex, packet->m_playerPrivileges); shared_ptr entity = getEntity(packet->m_entityId); if(entity != nullptr && entity->instanceof(eTYPE_PLAYER)) @@ -4129,3 +4238,301 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< m_recievedTick = GetTickCount(); m_packet = packet; } + +#ifdef _WINDOWS64 +void ClientConnection::handleAuthScheme(shared_ptr packet) +{ + app.DebugPrintf("Auth: Received AuthSchemePacket with %d scheme(s)\n", (int)packet->schemes.size()); + + newauthManager& authMgr = newauthManager::Get(); + std::string serverId(packet->serverId.begin(), packet->serverId.end()); + wstring chosenScheme; + bool isElyby = false; + bool offlineAvailable = false; + bool wantsOnlineAuth = false; + for (const auto& scheme : packet->schemes) + { + if (scheme == L"offline") + offlineAvailable = true; + } + + if (!g_NetworkManager.IsHost()) + { + auto accounts = authMgr.GetJavaAccounts(); + int activeIdx = authMgr.GetActiveJavaAccountIndex(); + if (activeIdx >= 0 && activeIdx < (int)accounts.size()) + { + bool acctIsOffline = accounts[activeIdx].isOffline || accounts[activeIdx].authProvider == "offline"; + bool acctIsElyby = (accounts[activeIdx].authProvider == "elyby"); + for (const auto& scheme : packet->schemes) + { + if (scheme == L"mojang" && !acctIsOffline && !acctIsElyby) + { + chosenScheme = L"mojang"; + wantsOnlineAuth = true; + break; + } + else if (scheme == L"elyby" && !acctIsOffline && acctIsElyby) + { + chosenScheme = L"elyby"; + isElyby = true; + wantsOnlineAuth = true; + break; + } + } + } + } + + if (chosenScheme.empty() && offlineAvailable) + { + chosenScheme = L"offline"; + } + + if (chosenScheme.empty()) + { + app.DebugPrintf("Auth: No compatible scheme found, disconnecting\n"); + app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed); + app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE); + return; + } + + std::string chosenStr(chosenScheme.begin(), chosenScheme.end()); + app.DebugPrintf("Auth: Chose scheme '%s'\n", chosenStr.c_str()); + + if (chosenScheme == L"offline") + { + newauth::JavaSession session = authMgr.GetSlotSession((int)m_userIndex); + wstring uuid, username; + if (!session.uuid.empty()) + { + uuid = wstring(session.uuid.begin(), session.uuid.end()); + username = wstring(session.username.begin(), session.username.end()); + } + else + { + auto accounts = authMgr.GetJavaAccounts(); + int activeIdx = authMgr.GetActiveJavaAccountIndex(); + if (activeIdx >= 0 && activeIdx < (int)accounts.size() && !accounts[activeIdx].uuid.empty()) + { + const auto& acct = accounts[activeIdx]; + uuid = wstring(acct.uuid.begin(), acct.uuid.end()); + username = wstring(acct.username.begin(), acct.username.end()); + } + else + { + username = minecraft->user->name; + std::string narrowName(username.begin(), username.end()); + std::string offUuid = GameUUID::generateOffline(narrowName).toDashed(); + uuid = wstring(offUuid.begin(), offUuid.end()); + } + } + send(std::make_shared(L"offline", uuid, username)); + return; + } + newauth::JavaSession session = authMgr.GetSlotSession((int)m_userIndex); + if (wantsOnlineAuth && (session.accessToken.empty() || session.uuid.empty() || session.username.empty())) + { + app.DebugPrintf("Auth: Waiting for slot %d online session to become ready\n", (int)m_userIndex); + authMgr.WaitForSlotReady((int)m_userIndex, 5000); + session = authMgr.GetSlotSession((int)m_userIndex); + if ((session.accessToken.empty() || session.uuid.empty() || session.username.empty()) && m_userIndex != 0) + { + app.DebugPrintf("Auth: Slot %d session still empty, retrying with primary slot session\n", (int)m_userIndex); + authMgr.WaitForSlotReady(0, 5000); + session = authMgr.GetSlotSession(0); + } + } + + if (session.accessToken.empty() || session.uuid.empty() || session.username.empty()) + { + app.DebugPrintf("Auth: Online session unavailable for chosen scheme '%s'\n", chosenStr.c_str()); + if (offlineAvailable) + { + app.DebugPrintf("Auth: Falling back to offline scheme because no online session is available\n"); + wstring fbUsername = minecraft->user->name; + std::string narrowName(fbUsername.begin(), fbUsername.end()); + std::string offUuid = GameUUID::generateOffline(narrowName).toDashed(); + wstring fbUuid(offUuid.begin(), offUuid.end()); + send(std::make_shared(L"offline", fbUuid, fbUsername)); + return; + } + app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed); + app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE); + return; + } + + std::string token = session.accessToken; + std::string uuid = session.uuid; + std::string undashedUuid = newauth::UndashUuid(uuid); + + std::string joinError; + bool joinOk = false; + if (isElyby) + joinOk = newauth::ElybyJoinServer(token, undashedUuid, serverId, joinError); + else + joinOk = newauth::JoinServer(token, undashedUuid, serverId, joinError); + + if (!joinOk) + { + app.DebugPrintf("Auth: JoinServer failed: %s\n", joinError.c_str()); + if (offlineAvailable) + { + app.DebugPrintf("Auth: Falling back to offline scheme\n"); + newauth::JavaSession fallbackSession = authMgr.GetSlotSession((int)m_userIndex); + wstring fbUuid, fbUsername; + if (!fallbackSession.uuid.empty()) + { + fbUuid = wstring(fallbackSession.uuid.begin(), fallbackSession.uuid.end()); + fbUsername = wstring(fallbackSession.username.begin(), fallbackSession.username.end()); + } + else + { + fbUsername = minecraft->user->name; + std::string narrowName(fbUsername.begin(), fbUsername.end()); + std::string offUuid = GameUUID::generateOffline(narrowName).toDashed(); + fbUuid = wstring(offUuid.begin(), offUuid.end()); + } + send(std::make_shared(L"offline", fbUuid, fbUsername)); + return; + } + app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed); + app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE); + return; + } + std::string dashedUuid = newauth::DashUuid(uuid); + wstring wUuid(dashedUuid.begin(), dashedUuid.end()); + wstring wUsername(session.username.begin(), session.username.end()); + + send(std::make_shared(chosenScheme, wUuid, wUsername)); + app.DebugPrintf("Auth: Sent AuthResponsePacket\n"); +} + +void ClientConnection::handleAuthResult(shared_ptr packet) +{ + if (!packet->success) + { + std::wstring errMsg = packet->errorMessage; + app.DebugPrintf("Auth: Server rejected authentication: %ls\n", errMsg.c_str()); + app.SetDisconnectReason(DisconnectPacket::eDisconnect_AuthFailed); + app.SetAction(ProfileManager.GetPrimaryPad(), eAppAction_ExitWorld, (void*)TRUE); + return; + } + + m_authAssignedUuid = packet->assignedUuid; + m_authAssignedUsername = packet->assignedUsername; + + if (!m_authAssignedUsername.empty()) + { + minecraft->user->name = m_authAssignedUsername; + extern char g_Win64Username[17]; + extern wchar_t g_Win64UsernameW[17]; + wcsncpy_s(g_Win64UsernameW, 17, m_authAssignedUsername.c_str(), _TRUNCATE); + WideCharToMultiByte(CP_ACP, 0, g_Win64UsernameW, -1, g_Win64Username, 17, nullptr, nullptr); + + IQNetPlayer* localQNetPlayer = nullptr; + INetworkPlayer* localNetworkPlayer = g_NetworkManager.GetLocalPlayerByUserIndex(m_userIndex); + if (localNetworkPlayer != nullptr) + { + localQNetPlayer = static_cast(localNetworkPlayer)->GetQNetPlayer(); + } + if (localQNetPlayer == nullptr && m_userIndex >= 0 && m_userIndex < MINECRAFT_NET_MAX_PLAYERS) + { + localQNetPlayer = &IQNet::m_player[m_userIndex]; + } + if (localQNetPlayer != nullptr) + { + wcsncpy_s(localQNetPlayer->m_gamertag, 32, m_authAssignedUsername.c_str(), _TRUNCATE); + } + } + + app.DebugPrintf("Auth: Success! uuid=%ls username=%ls\n", + m_authAssignedUuid.c_str(), m_authAssignedUsername.c_str()); + + if (!packet->skinKey.empty() && !packet->skinData.empty()) + { + if (newauth::ValidateSkinPng(packet->skinData.data(), packet->skinData.size())) + { + DWORD skinBytes = (DWORD)packet->skinData.size(); + PBYTE skinCopy = new BYTE[skinBytes]; + memcpy(skinCopy, packet->skinData.data(), skinBytes); + app.AddMemoryTextureFile(packet->skinKey, + skinCopy, + skinBytes); + m_authSkinKey = packet->skinKey; + app.DebugPrintf("Auth: Registered skin texture '%ls' (%d bytes)\n", + packet->skinKey.c_str(), (int)packet->skinData.size()); + } + } + else if (!m_authAssignedUuid.empty()) + { + std::string uuid(m_authAssignedUuid.begin(), m_authAssignedUuid.end()); + newauthManager& authMgr = newauthManager::Get(); + auto accounts = authMgr.GetJavaAccounts(); + int activeIdx = authMgr.GetActiveJavaAccountIndex(); + bool isElyby = (activeIdx >= 0 && activeIdx < (int)accounts.size() + && accounts[activeIdx].authProvider == "elyby"); + + std::string skinKey = isElyby + ? newauth::MakeElybySkinKey(uuid) + : newauth::MakeSkinKey(uuid); + std::string skinErr; + std::string skinUrl = isElyby + ? newauth::ElybyFetchProfileSkinUrl(uuid, skinErr) + // honbestlky, this makes me reconsider elyby considering it could just be used to generate alts but whatever + : newauth::FetchProfileSkinUrl(uuid, skinErr); + if (!skinUrl.empty()) + { + std::string fetchErr; + auto skinPng = newauth::FetchSkinPng(skinUrl, fetchErr); + if (!skinPng.empty() && newauth::ValidateSkinPng(skinPng.data(), skinPng.size())) + { + DWORD skinBytes = (DWORD)skinPng.size(); + PBYTE skinCopy = new BYTE[skinBytes]; + memcpy(skinCopy, skinPng.data(), skinBytes); + std::wstring wSkinKey(skinKey.begin(), skinKey.end()); + app.AddMemoryTextureFile(wSkinKey, skinCopy, skinBytes); + m_authSkinKey = wSkinKey; + app.DebugPrintf("Auth: Client-side skin fetch for uuid=%s (%d bytes)\n", + uuid.c_str(), (int)skinPng.size()); + } + } + } + + sendLoginPacketAfterAuth(); +} + +void ClientConnection::sendLoginPacketAfterAuth() +{ + PlayerUID offlineXUID = INVALID_XUID; + PlayerUID onlineXUID = INVALID_XUID; + + if (ProfileManager.IsSignedInLive(m_userIndex)) + { + ProfileManager.GetXUID(m_userIndex, &onlineXUID, true); + } + if (!ProfileManager.IsGuest(m_userIndex)) + { + ProfileManager.GetXUID(m_userIndex, &offlineXUID, false); + } + + BOOL allAllowed, friendsAllowed; + ProfileManager.AllowedPlayerCreatedContent(m_userIndex, true, &allAllowed, &friendsAllowed); + + send(std::make_shared( + minecraft->user->name, + SharedConstants::NETWORK_PROTOCOL_VERSION, + offlineXUID, onlineXUID, + (allAllowed != TRUE && friendsAllowed == TRUE), + m_preLoginUgcPlayersVersion, + app.GetPlayerSkinId(m_userIndex), + app.GetPlayerCapeId(m_userIndex), + ProfileManager.IsGuest(m_userIndex))); + + if (!g_NetworkManager.IsHost()) + { + Minecraft::GetInstance()->progressRenderer->progressStagePercentage((eCCLoginSent * 100) / (eCCConnected)); + } + + app.DebugPrintf("Auth: Sent LoginPacket after auth\n"); +} +#endif diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index 3448496d07..f70e70942a 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include "..\Minecraft.World\net.minecraft.network.h" class Minecraft; @@ -6,6 +7,8 @@ class MultiPlayerLevel; class SavedDataStorage; class Socket; class MultiplayerLocalPlayer; +class AuthSchemePacket; +class AuthResultPacket; class ClientConnection : public PacketListener { @@ -14,6 +17,7 @@ class ClientConnection : public PacketListener { eCCPreLoginSent = 0, eCCPreLoginReceived, + eCCAuthWaiting, eCCLoginSent, eCCLoginReceived, eCCConnected @@ -48,6 +52,7 @@ class ClientConnection : public PacketListener std::unordered_set m_trackedEntityIds; std::unordered_set m_visibleChunks; + std::unordered_map m_pendingRemotePlayerSmallIds; static int64_t chunkKey(int x, int z) { return ((int64_t)x << 32) | ((int64_t)z & 0xFFFFFFFF); } @@ -55,6 +60,9 @@ class ClientConnection : public PacketListener bool shouldProcessForEntity(int entityId) const; bool shouldProcessForPosition(int blockX, int blockZ) const; bool anyOtherConnectionHasChunk(int x, int z) const; +#ifdef _WINDOWS64 + bool ensureRemoteNetworkPlayer(BYTE smallId, const std::wstring *playerName, PlayerUID xuid); +#endif public: bool isTrackingEntity(int entityId) const { return m_trackedEntityIds.count(entityId) > 0; } @@ -164,6 +172,16 @@ class ClientConnection : public PacketListener virtual void handleParticleEvent(shared_ptr packet); virtual void handleUpdateAttributes(shared_ptr packet); + // fuck ass shit to make the skins work waaah waaah waah + virtual void handleAuthScheme(shared_ptr packet); + virtual void handleAuthResult(shared_ptr packet); +private: + void sendLoginPacketAfterAuth(); + std::wstring m_authAssignedUuid; + std::wstring m_authAssignedUsername; + std::wstring m_authSkinKey; + DWORD m_preLoginUgcPlayersVersion = 0; + private: // 4J: Entity link packet deferred class DeferredEntityLinkPacket diff --git a/Minecraft.Client/Common/Media/platformskin.swf b/Minecraft.Client/Common/Media/platformskin.swf new file mode 100644 index 0000000000..f7c64ef0d8 Binary files /dev/null and b/Minecraft.Client/Common/Media/platformskin.swf differ diff --git a/Minecraft.Client/Common/Media/platformskinHD.swf b/Minecraft.Client/Common/Media/platformskinHD.swf new file mode 100644 index 0000000000..2d85415898 Binary files /dev/null and b/Minecraft.Client/Common/Media/platformskinHD.swf differ diff --git a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp index 430f2c111a..ed95cce59d 100644 --- a/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp +++ b/Minecraft.Client/Common/Network/PlatformNetworkManagerStub.cpp @@ -19,6 +19,7 @@ CPlatformNetworkManagerStub *g_pPlatformNetworkManager; void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) { const char * pszDescription; + bool exposePlayer = Win64_ShouldExposeSessionPlayer(pQNetPlayer); // 4J Stu - We create a fake socket for every where that we need an INBOUND queue of game data. Outbound // is all handled by QNet so we don't need that. Therefore each client player has one, and the host has one @@ -79,7 +80,10 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) m_machineQNetPrimaryPlayers.push_back( pQNetPlayer ); } } - g_NetworkManager.PlayerJoining( networkPlayer ); + if (exposePlayer) + { + g_NetworkManager.PlayerJoining( networkPlayer ); + } if( createFakeSocket == true && !m_bHostChanged ) { @@ -101,10 +105,13 @@ void CPlatformNetworkManagerStub::NotifyPlayerJoined(IQNetPlayer *pQNetPlayer ) SystemFlagAddPlayer( networkPlayer ); } - for( int idx = 0; idx < XUSER_MAX_COUNT; ++idx) + if (exposePlayer) { - if(playerChangedCallback[idx] != nullptr) - playerChangedCallback[idx]( playerChangedCallbackParam[idx], networkPlayer, false ); + for( int idx = 0; idx < XUSER_MAX_COUNT; ++idx) + { + if(playerChangedCallback[idx] != nullptr) + playerChangedCallback[idx]( playerChangedCallbackParam[idx], networkPlayer, false ); + } } if(m_pIQNet->GetState() == QNET_STATE_GAME_PLAY) @@ -142,12 +149,18 @@ void CPlatformNetworkManagerStub::NotifyPlayerLeaving(IQNetPlayer* pQNetPlayer) SystemFlagRemovePlayer(networkPlayer); } - g_NetworkManager.PlayerLeaving(networkPlayer); + if (Win64_ShouldExposeSessionPlayer(pQNetPlayer)) + { + g_NetworkManager.PlayerLeaving(networkPlayer); + } - for (int idx = 0; idx < XUSER_MAX_COUNT; ++idx) + if (Win64_ShouldExposeSessionPlayer(pQNetPlayer)) { - if (playerChangedCallback[idx] != nullptr) - playerChangedCallback[idx](playerChangedCallbackParam[idx], networkPlayer, true); + for (int idx = 0; idx < XUSER_MAX_COUNT; ++idx) + { + if (playerChangedCallback[idx] != nullptr) + playerChangedCallback[idx](playerChangedCallbackParam[idx], networkPlayer, true); + } } removeNetworkPlayer(pQNetPlayer); @@ -544,6 +557,7 @@ int CPlatformNetworkManagerStub::JoinGame(FriendSessionInfo* searchResult, int l m_bLeaveGameOnTick = false; IQNet::s_isHosting = false; m_pIQNet->ClientJoinGame(); + Win64_SetJoinedDedicatedServer(searchResult->data.isDedicatedServer); IQNet::m_player[0].m_smallId = 0; IQNet::m_player[0].m_isRemote = true; @@ -865,6 +879,7 @@ void CPlatformNetworkManagerStub::SearchForGames() info->data.subTexturePackId = lanSessions[i].subTexturePackId; info->data.isReadyToJoin = lanSessions[i].isJoinable; info->data.isJoinable = lanSessions[i].isJoinable; + info->data.isDedicatedServer = lanSessions[i].isDedicatedServer; strncpy_s(info->data.hostIP, sizeof(info->data.hostIP), lanSessions[i].hostIP, _TRUNCATE); info->data.hostPort = lanSessions[i].hostPort; wcsncpy_s(info->data.hostName, XUSER_NAME_SIZE, lanSessions[i].hostName, _TRUNCATE); @@ -916,6 +931,7 @@ void CPlatformNetworkManagerStub::SearchForGames() info->displayLabelViewableStartIndex = 0; info->data.isReadyToJoin = true; info->data.isJoinable = true; + info->data.isDedicatedServer = true; strncpy_s(info->data.hostIP, sizeof(info->data.hostIP), ipBuf, _TRUNCATE); info->data.hostPort = port; info->sessionId = static_cast(inet_addr(ipBuf)) | static_cast(port) << 32; diff --git a/Minecraft.Client/Common/Network/SessionInfo.h b/Minecraft.Client/Common/Network/SessionInfo.h index 4e091c8715..e1e50f8aff 100644 --- a/Minecraft.Client/Common/Network/SessionInfo.h +++ b/Minecraft.Client/Common/Network/SessionInfo.h @@ -70,6 +70,7 @@ typedef struct _GameSessionData bool isReadyToJoin; bool isJoinable; + bool isDedicatedServer; char hostIP[64]; int hostPort; @@ -85,6 +86,7 @@ typedef struct _GameSessionData subTexturePackId = 0; isReadyToJoin = false; isJoinable = true; + isDedicatedServer = false; memset(hostIP, 0, sizeof(hostIP)); hostPort = 0; memset(hostName, 0, sizeof(hostName)); diff --git a/Minecraft.Client/Common/UI/NativeUIRenderer.cpp b/Minecraft.Client/Common/UI/NativeUIRenderer.cpp new file mode 100644 index 0000000000..5bf6b2fc70 --- /dev/null +++ b/Minecraft.Client/Common/UI/NativeUIRenderer.cpp @@ -0,0 +1,1868 @@ +#include "stdafx.h" +#include "NativeUIRenderer.h" +#include "UI.h" +#include +#include +#include +#include +#pragma comment(lib, "windowscodecs.lib") + +#ifdef _WINDOWS64 +#include +#include "../../Windows64/KeyboardMouseInput.h" +#pragma comment(lib, "shell32.lib") +extern KeyboardMouseInput g_KBMInput; +extern HWND g_hWnd; +#endif + +#pragma comment(lib, "d3dcompiler.lib") + +extern ID3D11Device* g_pd3dDevice; +extern ID3D11DeviceContext* g_pImmediateContext; +extern ID3D11RenderTargetView* g_pRenderTargetView; +extern ID3D11DepthStencilView* g_pDepthStencilView; + +#include + +#include "../../Font.h" +#include "../../Textures.h" +#include "../../ResourceLocation.h" + +namespace +{ + static constexpr float kVW = 1280.0f; + static constexpr float kVH = 720.0f; + static constexpr float kFontUnitH = 8.0f; + static constexpr int kMaxVerts = 8192; + static constexpr int kMaxClipStack = 16; + + inline Font* GetFont() + { + return Minecraft::GetInstance()->font; + } + + inline float ScaleForSize(float size) + { + return size / kFontUnitH; + } + + struct UIVertex + { + float x, y; + float r, g, b, a; + float u, v; + }; + + enum BatchMode { BATCH_COLOR, BATCH_TEXT, BATCH_TEXTURE }; + + static ID3D11VertexShader* s_pVS = nullptr; + static ID3D11PixelShader* s_pPS_Color = nullptr; + static ID3D11PixelShader* s_pPS_Tex = nullptr; + static ID3D11PixelShader* s_pPS_TexFull = nullptr; + static ID3D11InputLayout* s_pInputLayout = nullptr; + static ID3D11Buffer* s_pVB = nullptr; + static ID3D11RasterizerState* s_pRastState = nullptr; + static ID3D11RasterizerState* s_pRastScissor = nullptr; + static ID3D11DepthStencilState* s_pDepthState = nullptr; + static ID3D11BlendState* s_pBlendState = nullptr; + static ID3D11SamplerState* s_pSampler = nullptr; + static ID3D11SamplerState* s_pSamplerLinear = nullptr; + static bool s_initialized = false; + + static UIVertex s_vertices[kMaxVerts]; + static int s_vertCount = 0; + static bool s_inFrame = false; + static BatchMode s_batchMode = BATCH_COLOR; + static int s_batchTexId = -1; + + struct FileTexEntry { int id; int w; int h; }; + static std::unordered_map s_fileTexCache; + + struct ClipRect { float x, y, w, h; }; + static ClipRect s_clipStack[kMaxClipStack]; + static int s_clipDepth = 0; + + static float s_bbWidth = kVW; + static float s_bbHeight = kVH; + + static float s_vpX = 0.0f; + static float s_vpY = 0.0f; + static float s_vpW = kVW; + static float s_vpH = kVH; + + static ID3D11RenderTargetView* s_savedRTV = nullptr; + static ID3D11DepthStencilView* s_savedDSV = nullptr; + static D3D11_VIEWPORT s_savedViewport = {}; + static ID3D11RasterizerState* s_savedRast = nullptr; + static ID3D11DepthStencilState* s_savedDepth = nullptr; + static UINT s_savedStencilRef = 0; + static ID3D11BlendState* s_savedBlend = nullptr; + static float s_savedBlendFactor[4] = {}; + static UINT s_savedSampleMask = 0; + static ID3D11VertexShader* s_savedVS = nullptr; + static ID3D11PixelShader* s_savedPS = nullptr; + static ID3D11InputLayout* s_savedIL = nullptr; + static D3D11_PRIMITIVE_TOPOLOGY s_savedTopo = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED; + + static const char* s_vsCode = + "struct VS_IN { float2 pos : POSITION; float4 col : COLOR; float2 uv : TEXCOORD0; };\n" + "struct VS_OUT { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n" + "VS_OUT main(VS_IN i)\n" + "{\n" + " VS_OUT o;\n" + " o.pos.x = i.pos.x / 640.0 - 1.0;\n" + " o.pos.y = 1.0 - i.pos.y / 360.0;\n" + " o.pos.z = 0.0;\n" + " o.pos.w = 1.0;\n" + " o.col = i.col;\n" + " o.uv = i.uv;\n" + " return o;\n" + "}\n"; + + static const char* s_psColorCode = + "struct PS_IN { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n" + "float4 main(PS_IN i) : SV_Target { return i.col; }\n"; + + static const char* s_psTexCode = + "Texture2D tex : register(t0);\n" + "SamplerState samp : register(s0);\n" + "struct PS_IN { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n" + "float4 main(PS_IN i) : SV_Target\n" + "{\n" + " float4 t = tex.Sample(samp, i.uv);\n" + " return float4(i.col.rgb, i.col.a * t.a);\n" + "}\n"; + + static const char* s_psTexFullCode = + "Texture2D tex : register(t0);\n" + "SamplerState samp : register(s0);\n" + "struct PS_IN { float4 pos : SV_Position; float4 col : COLOR; float2 uv : TEXCOORD0; };\n" + "float4 main(PS_IN i) : SV_Target\n" + "{\n" + " float4 t = tex.Sample(samp, i.uv);\n" + " return t * i.col;\n" + "}\n"; + + static void DecodeColor(uint32_t color, float& r, float& g, float& b, float& a) + { + a = ((color >> 24) & 0xFF) / 255.0f; + r = ((color >> 16) & 0xFF) / 255.0f; + g = ((color >> 8) & 0xFF) / 255.0f; + b = ((color ) & 0xFF) / 255.0f; + } + + static ID3D11PixelShader* CompilePS(const char* code, const char* name) + { + ID3DBlob* blob = nullptr; + ID3DBlob* err = nullptr; + HRESULT hr = D3DCompile(code, strlen(code), name, nullptr, nullptr, + "main", "ps_4_0", D3DCOMPILE_ENABLE_STRICTNESS, 0, &blob, &err); + if (FAILED(hr)) { if (err) err->Release(); return nullptr; } + if (err) err->Release(); + + ID3D11PixelShader* ps = nullptr; + hr = g_pd3dDevice->CreatePixelShader(blob->GetBufferPointer(), + blob->GetBufferSize(), nullptr, &ps); + blob->Release(); + return SUCCEEDED(hr) ? ps : nullptr; + } + + static void InitD3D() + { + if (s_initialized || !g_pd3dDevice) return; + + auto SafeRelease = [](auto*& p) { if (p) { p->Release(); p = nullptr; } }; + SafeRelease(s_pVS); + SafeRelease(s_pInputLayout); + SafeRelease(s_pPS_Color); + SafeRelease(s_pPS_Tex); + SafeRelease(s_pPS_TexFull); + SafeRelease(s_pVB); + SafeRelease(s_pRastState); + SafeRelease(s_pRastScissor); + SafeRelease(s_pDepthState); + SafeRelease(s_pBlendState); + SafeRelease(s_pSampler); + SafeRelease(s_pSamplerLinear); + + HRESULT hr; + ID3DBlob* vsBlob = nullptr; + ID3DBlob* err = nullptr; + + hr = D3DCompile(s_vsCode, strlen(s_vsCode), "NativeUI_VS", nullptr, nullptr, + "main", "vs_4_0", D3DCOMPILE_ENABLE_STRICTNESS, 0, &vsBlob, &err); + if (FAILED(hr)) { if (err) err->Release(); return; } + if (err) { err->Release(); err = nullptr; } + + hr = g_pd3dDevice->CreateVertexShader(vsBlob->GetBufferPointer(), + vsBlob->GetBufferSize(), nullptr, &s_pVS); + if (FAILED(hr)) { vsBlob->Release(); return; } + + D3D11_INPUT_ELEMENT_DESC layout[] = { + { "POSITION", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, sizeof(float) * 2, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, sizeof(float) * 6, D3D11_INPUT_PER_VERTEX_DATA, 0 }, + }; + hr = g_pd3dDevice->CreateInputLayout(layout, 3, vsBlob->GetBufferPointer(), + vsBlob->GetBufferSize(), &s_pInputLayout); + vsBlob->Release(); + if (FAILED(hr)) return; + + s_pPS_Color = CompilePS(s_psColorCode, "NativeUI_PS_Color"); + if (!s_pPS_Color) return; + + s_pPS_Tex = CompilePS(s_psTexCode, "NativeUI_PS_Tex"); + if (!s_pPS_Tex) return; + + s_pPS_TexFull = CompilePS(s_psTexFullCode, "NativeUI_PS_TexFull"); + if (!s_pPS_TexFull) return; + + D3D11_BUFFER_DESC vbDesc = {}; + vbDesc.ByteWidth = sizeof(s_vertices); + vbDesc.Usage = D3D11_USAGE_DYNAMIC; + vbDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER; + vbDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + hr = g_pd3dDevice->CreateBuffer(&vbDesc, nullptr, &s_pVB); + if (FAILED(hr)) return; + + D3D11_RASTERIZER_DESC rasDesc = {}; + rasDesc.FillMode = D3D11_FILL_SOLID; + rasDesc.CullMode = D3D11_CULL_NONE; + rasDesc.DepthClipEnable = FALSE; + rasDesc.ScissorEnable = FALSE; + g_pd3dDevice->CreateRasterizerState(&rasDesc, &s_pRastState); + + rasDesc.ScissorEnable = TRUE; + g_pd3dDevice->CreateRasterizerState(&rasDesc, &s_pRastScissor); + + D3D11_DEPTH_STENCIL_DESC dsDesc = {}; + dsDesc.DepthEnable = FALSE; + dsDesc.StencilEnable = FALSE; + g_pd3dDevice->CreateDepthStencilState(&dsDesc, &s_pDepthState); + + D3D11_BLEND_DESC blendDesc = {}; + blendDesc.RenderTarget[0].BlendEnable = TRUE; + blendDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA; + blendDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE; + blendDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_INV_SRC_ALPHA; + blendDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD; + blendDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL; + g_pd3dDevice->CreateBlendState(&blendDesc, &s_pBlendState); + + D3D11_SAMPLER_DESC sampDesc = {}; + sampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT; + sampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + g_pd3dDevice->CreateSamplerState(&sampDesc, &s_pSampler); + + D3D11_SAMPLER_DESC sampLinDesc = {}; + sampLinDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR; + sampLinDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP; + sampLinDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP; + sampLinDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP; + g_pd3dDevice->CreateSamplerState(&sampLinDesc, &s_pSamplerLinear); + + s_initialized = true; + } + + static void BindBatchTexture(ID3D11DeviceContext* ctx) + { + if (s_batchMode == BATCH_TEXT) + { + Font* font = GetFont(); + if (font) + { + ResourceLocation* loc = font->getTextureLocation(); + if (loc && loc->isPreloaded()) + { + int texId = font->getTextures()->loadTexture(loc->getTexture()); + ID3D11ShaderResourceView* srv = RenderManager.TextureGetTexture(texId); + if (srv) + { + ctx->PSSetShaderResources(0, 1, &srv); + ctx->PSSetSamplers(0, 1, &s_pSampler); + } + } + } + } + else if (s_batchMode == BATCH_TEXTURE && s_batchTexId >= 0) + { + ID3D11ShaderResourceView* srv = RenderManager.TextureGetTexture(s_batchTexId); + if (srv) + { + ctx->PSSetShaderResources(0, 1, &srv); + ctx->PSSetSamplers(0, 1, &s_pSampler); + } + } + } + + static void FlushBatch() + { + if (s_vertCount == 0 || !s_initialized) return; + + ID3D11DeviceContext* ctx = g_pImmediateContext; + + D3D11_MAPPED_SUBRESOURCE mapped; + HRESULT hr = ctx->Map(s_pVB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped); + if (FAILED(hr)) { s_vertCount = 0; return; } + memcpy(mapped.pData, s_vertices, s_vertCount * sizeof(UIVertex)); + ctx->Unmap(s_pVB, 0); + + if (s_batchMode == BATCH_COLOR) + ctx->PSSetShader(s_pPS_Color, nullptr, 0); + else if (s_batchMode == BATCH_TEXT) + { + ctx->PSSetShader(s_pPS_Tex, nullptr, 0); + BindBatchTexture(ctx); + } + else + { + ctx->PSSetShader(s_pPS_TexFull, nullptr, 0); + BindBatchTexture(ctx); + } + + ctx->Draw(s_vertCount, 0); + s_vertCount = 0; + } + + static void EnsureBatchMode(BatchMode mode, int texId = -1) + { + if (s_vertCount > 0 && (s_batchMode != mode || (mode == BATCH_TEXTURE && s_batchTexId != texId))) + FlushBatch(); + s_batchMode = mode; + s_batchTexId = texId; + } + + static void SaveD3DState() + { + ID3D11DeviceContext* ctx = g_pImmediateContext; + ctx->OMGetRenderTargets(1, &s_savedRTV, &s_savedDSV); + UINT numVP = 1; + ctx->RSGetViewports(&numVP, &s_savedViewport); + ctx->RSGetState(&s_savedRast); + ctx->OMGetDepthStencilState(&s_savedDepth, &s_savedStencilRef); + ctx->OMGetBlendState(&s_savedBlend, s_savedBlendFactor, &s_savedSampleMask); + ctx->VSGetShader(&s_savedVS, nullptr, nullptr); + ctx->PSGetShader(&s_savedPS, nullptr, nullptr); + ctx->IAGetInputLayout(&s_savedIL); + ctx->IAGetPrimitiveTopology(&s_savedTopo); + } + + static void RestoreD3DState() + { + ID3D11DeviceContext* ctx = g_pImmediateContext; + ctx->OMSetRenderTargets(1, &s_savedRTV, s_savedDSV); + ctx->RSSetViewports(1, &s_savedViewport); + ctx->RSSetState(s_savedRast); + ctx->OMSetDepthStencilState(s_savedDepth, s_savedStencilRef); + ctx->OMSetBlendState(s_savedBlend, s_savedBlendFactor, s_savedSampleMask); + ctx->VSSetShader(s_savedVS, nullptr, 0); + ctx->PSSetShader(s_savedPS, nullptr, 0); + ctx->IASetInputLayout(s_savedIL); + ctx->IASetPrimitiveTopology(s_savedTopo); + + if (s_savedRTV) { s_savedRTV->Release(); s_savedRTV = nullptr; } + if (s_savedDSV) { s_savedDSV->Release(); s_savedDSV = nullptr; } + if (s_savedRast) { s_savedRast->Release(); s_savedRast = nullptr; } + if (s_savedDepth) { s_savedDepth->Release(); s_savedDepth = nullptr; } + if (s_savedBlend) { s_savedBlend->Release(); s_savedBlend = nullptr; } + if (s_savedVS) { s_savedVS->Release(); s_savedVS = nullptr; } + if (s_savedPS) { s_savedPS->Release(); s_savedPS = nullptr; } + if (s_savedIL) { s_savedIL->Release(); s_savedIL = nullptr; } + } + + static void SetupD3DPipeline() + { + ID3D11DeviceContext* ctx = g_pImmediateContext; + + ctx->OMSetRenderTargets(1, &g_pRenderTargetView, nullptr); + + ID3D11Resource* rtvResource = nullptr; + g_pRenderTargetView->GetResource(&rtvResource); + ID3D11Texture2D* rtvTex = nullptr; + rtvResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&rtvTex); + rtvResource->Release(); + + if (rtvTex) + { + D3D11_TEXTURE2D_DESC desc; + rtvTex->GetDesc(&desc); + s_bbWidth = (float)desc.Width; + s_bbHeight = (float)desc.Height; + rtvTex->Release(); + } + else + { + s_bbWidth = kVW; + s_bbHeight = kVH; + } + + { + float bbAspect = s_bbWidth / s_bbHeight; + constexpr float targetAspect = kVW / kVH; + + if (bbAspect > targetAspect) + { + s_vpH = s_bbHeight; + s_vpW = s_bbHeight * targetAspect; + s_vpX = (s_bbWidth - s_vpW) * 0.5f; + s_vpY = 0.0f; + } + else if (bbAspect < targetAspect) + { + s_vpW = s_bbWidth; + s_vpH = s_bbWidth / targetAspect; + s_vpX = 0.0f; + s_vpY = (s_bbHeight - s_vpH) * 0.5f; + } + else + { + s_vpX = 0.0f; + s_vpY = 0.0f; + s_vpW = s_bbWidth; + s_vpH = s_bbHeight; + } + } + + D3D11_VIEWPORT vp = {}; + vp.TopLeftX = s_vpX; + vp.TopLeftY = s_vpY; + vp.Width = s_vpW; + vp.Height = s_vpH; + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + ctx->RSSetViewports(1, &vp); + + ctx->IASetInputLayout(s_pInputLayout); + ctx->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + UINT stride = sizeof(UIVertex); + UINT offset = 0; + ctx->IASetVertexBuffers(0, 1, &s_pVB, &stride, &offset); + + ctx->VSSetShader(s_pVS, nullptr, 0); + ctx->PSSetShader(s_pPS_Color, nullptr, 0); + ctx->RSSetState(s_pRastState); + ctx->OMSetDepthStencilState(s_pDepthState, 0); + + constexpr float bf[4] = { 0, 0, 0, 0 }; + ctx->OMSetBlendState(s_pBlendState, bf, 0xFFFFFFFF); + } + + static void PushQuad(float x0, float y0, float x1, float y1, + float r, float g, float b, float a, + float u0, float v0, float u1, float v1) + { + if (s_vertCount + 6 > kMaxVerts) FlushBatch(); + + UIVertex* v = &s_vertices[s_vertCount]; + v[0] = { x0, y0, r, g, b, a, u0, v0 }; + v[1] = { x1, y0, r, g, b, a, u1, v0 }; + v[2] = { x0, y1, r, g, b, a, u0, v1 }; + v[3] = { x0, y1, r, g, b, a, u0, v1 }; + v[4] = { x1, y0, r, g, b, a, u1, v0 }; + v[5] = { x1, y1, r, g, b, a, u1, v1 }; + s_vertCount += 6; + } + + static void PushGradientQuad(float x0, float y0, float x1, float y1, + float tr, float tg, float tb, float ta, + float br, float bg, float bb, float ba) + { + if (s_vertCount + 6 > kMaxVerts) FlushBatch(); + + UIVertex* v = &s_vertices[s_vertCount]; + v[0] = { x0, y0, tr, tg, tb, ta, 0, 0 }; + v[1] = { x1, y0, tr, tg, tb, ta, 0, 0 }; + v[2] = { x0, y1, br, bg, bb, ba, 0, 0 }; + v[3] = { x0, y1, br, bg, bb, ba, 0, 0 }; + v[4] = { x1, y0, tr, tg, tb, ta, 0, 0 }; + v[5] = { x1, y1, br, bg, bb, ba, 0, 0 }; + s_vertCount += 6; + } + + static void ApplyScissor() + { + ID3D11DeviceContext* ctx = g_pImmediateContext; + if (s_clipDepth <= 0) + { + ctx->RSSetState(s_pRastState); + return; + } + + ctx->RSSetState(s_pRastScissor); + + const ClipRect& c = s_clipStack[s_clipDepth - 1]; + float scaleX = s_vpW / kVW; + float scaleY = s_vpH / kVH; + + D3D11_RECT r; + r.left = (LONG)(s_vpX + c.x * scaleX); + r.top = (LONG)(s_vpY + c.y * scaleY); + r.right = (LONG)(s_vpX + (c.x + c.w) * scaleX); + r.bottom = (LONG)(s_vpY + (c.y + c.h) * scaleY); + ctx->RSSetScissorRects(1, &r); + } + + static void DrawGlyphString(Font* font, const std::wstring& str, + float startX, float startY, float scale, + float cr, float cg, float cb, float ca) + { + EnsureBatchMode(BATCH_TEXT); + + const int cols = font->getCols(); + const int cw = font->getCharWidth(); + const int ch = font->getCharHeight(); + const float atlasW = (float)(cols * cw); + const float atlasH = (float)(font->getRows() * ch); + + float curX = startX; + + for (size_t i = 0; i < str.length(); ++i) + { + wchar_t c = str[i]; + + if (c == 167 && i + 1 < str.length()) + { + ++i; + continue; + } + + wchar_t mapped = font->mapChar(c); + int charW = font->getCharPixelWidth(c); + + float uOff = (float)(mapped % cols * cw); + float vOff = (float)(mapped / cols * ch); + + float u0 = uOff / atlasW; + float v0 = vOff / atlasH; + float u1 = (uOff + (float)charW - 0.01f) / atlasW; + float v1 = (vOff + 7.99f) / atlasH; + + float gx0 = curX; + float gy0 = startY; + float gx1 = curX + ((float)charW - 0.01f) * scale; + float gy1 = startY + (float)ch * scale; + + PushQuad(gx0, gy0, gx1, gy1, cr, cg, cb, ca, u0, v0, u1, v1); + + curX += (float)charW * scale; + } + } + + static void ApplyAlignment(Font* font, const std::wstring& wstr, + float x, float y, + float scale, uint32_t align, + float& drawX, float& drawY) + { + float textW = static_cast(font->width(wstr)) * scale; + float textH = kFontUnitH * scale; + + drawX = x; + if (align & NativeUI::ALIGN_CENTER_X) drawX = x - textW * 0.5f; + else if (align & NativeUI::ALIGN_RIGHT) drawX = x - textW; + + drawY = y; + if (align & NativeUI::ALIGN_CENTER_Y) drawY = y - textH * 0.5f; + else if (align & NativeUI::ALIGN_BOTTOM) drawY = y - textH; + } +} + +namespace NativeUI +{ + +void BeginFrame() +{ + InitD3D(); + if (!s_initialized) return; + + SaveD3DState(); + SetupD3DPipeline(); + + s_vertCount = 0; + s_batchMode = BATCH_COLOR; + s_batchTexId = -1; + s_clipDepth = 0; + s_inFrame = true; +} + +void EndFrame() +{ + if (!s_inFrame) return; + + FlushBatch(); + + ID3D11ShaderResourceView* nullSrv = nullptr; + g_pImmediateContext->PSSetShaderResources(0, 1, &nullSrv); + + RestoreD3DState(); + s_inFrame = false; +} + +void Shutdown() +{ + auto SafeRelease = [](auto*& p) { if (p) { p->Release(); p = nullptr; } }; + SafeRelease(s_pBlendState); + SafeRelease(s_pDepthState); + SafeRelease(s_pRastState); + SafeRelease(s_pRastScissor); + SafeRelease(s_pSampler); + SafeRelease(s_pSamplerLinear); + SafeRelease(s_pVB); + SafeRelease(s_pInputLayout); + SafeRelease(s_pPS_TexFull); + SafeRelease(s_pPS_Tex); + SafeRelease(s_pPS_Color); + SafeRelease(s_pVS); + s_initialized = false; +} + +void DrawRect(float x, float y, float w, float h, uint32_t color) +{ + if (!s_inFrame) return; + EnsureBatchMode(BATCH_COLOR); + float r, g, b, a; + DecodeColor(color, r, g, b, a); + PushQuad(x, y, x + w, y + h, r, g, b, a, 0, 0, 0, 0); +} + +void DrawRectFullscreen(uint32_t color) +{ + if (!s_inFrame) return; + + FlushBatch(); + + D3D11_VIEWPORT fullVP = {}; + fullVP.TopLeftX = 0; + fullVP.TopLeftY = 0; + fullVP.Width = s_bbWidth; + fullVP.Height = s_bbHeight; + fullVP.MinDepth = 0.0f; + fullVP.MaxDepth = 1.0f; + g_pImmediateContext->RSSetViewports(1, &fullVP); + + EnsureBatchMode(BATCH_COLOR); + float r, g, b, a; + DecodeColor(color, r, g, b, a); + PushQuad(0, 0, kVW, kVH, r, g, b, a, 0, 0, 0, 0); + FlushBatch(); + + D3D11_VIEWPORT vp = {}; + vp.TopLeftX = s_vpX; + vp.TopLeftY = s_vpY; + vp.Width = s_vpW; + vp.Height = s_vpH; + vp.MinDepth = 0.0f; + vp.MaxDepth = 1.0f; + g_pImmediateContext->RSSetViewports(1, &vp); +} + +static void EmitCornerFans(const float cx[4], const float cy[4], + const float startAngle[4], float radius, + const float cr[4], const float cg[4], + const float cb[4], const float ca[4]) +{ + static constexpr int kSeg = 6; + static constexpr float kHalfPi = 1.5707963f; + + for (int corner = 0; corner < 4; ++corner) + { + for (int seg = 0; seg < kSeg; ++seg) + { + float a0 = startAngle[corner] + kHalfPi * seg / kSeg; + float a1 = startAngle[corner] + kHalfPi * (seg + 1) / kSeg; + + float px0 = cx[corner] + cosf(a0) * radius; + float py0 = cy[corner] + sinf(a0) * radius; + float px1 = cx[corner] + cosf(a1) * radius; + float py1 = cy[corner] + sinf(a1) * radius; + + if (s_vertCount + 3 > kMaxVerts) FlushBatch(); + UIVertex* v = &s_vertices[s_vertCount]; + v[0] = { cx[corner], cy[corner], cr[corner], cg[corner], cb[corner], ca[corner], 0, 0 }; + v[1] = { px0, py0, cr[corner], cg[corner], cb[corner], ca[corner], 0, 0 }; + v[2] = { px1, py1, cr[corner], cg[corner], cb[corner], ca[corner], 0, 0 }; + s_vertCount += 3; + } + } +} + +void DrawRoundedRect(float x, float y, float w, float h, + float radius, uint32_t color) +{ + if (!s_inFrame) return; + EnsureBatchMode(BATCH_COLOR); + + float r, g, b, a; + DecodeColor(color, r, g, b, a); + + float maxR = fminf(w, h) * 0.5f; + if (radius > maxR) radius = maxR; + + static constexpr float kHalfPi = 1.5707963f; + + PushQuad(x + radius, y, x + w - radius, y + h, r, g, b, a, 0, 0, 0, 0); + PushQuad(x, y + radius, x + radius, y + h - radius, r, g, b, a, 0, 0, 0, 0); + PushQuad(x + w - radius, y + radius, x + w, y + h - radius, r, g, b, a, 0, 0, 0, 0); + + float cx[4] = { x + radius, x + w - radius, x + radius, x + w - radius }; + float cy[4] = { y + radius, y + radius, y + h - radius, y + h - radius }; + float startAngle[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 }; + float cr[4] = { r, r, r, r }; + float cg[4] = { g, g, g, g }; + float cb[4] = { b, b, b, b }; + float ca[4] = { a, a, a, a }; + + EmitCornerFans(cx, cy, startAngle, radius, cr, cg, cb, ca); +} + +void DrawRoundedBorder(float x, float y, float w, float h, + float radius, float thickness, uint32_t color) +{ + if (!s_inFrame) return; + + float maxR = fminf(w, h) * 0.5f; + if (radius > maxR) radius = maxR; + float ri = fmaxf(0.0f, radius - thickness); + + DrawRect(x + radius, y, w - 2 * radius, thickness, color); + DrawRect(x + radius, y + h - thickness, w - 2 * radius, thickness, color); + DrawRect(x, y + radius, thickness, h - 2 * radius, color); + DrawRect(x + w - thickness, y + radius, thickness, h - 2 * radius, color); + + EnsureBatchMode(BATCH_COLOR); + float r, g, b, a; + DecodeColor(color, r, g, b, a); + + static constexpr int kSeg = 6; + static constexpr float kHalfPi = 1.5707963f; + + float cx_[4] = { x + radius, x + w - radius, x + radius, x + w - radius }; + float cy_[4] = { y + radius, y + radius, y + h - radius, y + h - radius }; + float startA[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 }; + + for (int corner = 0; corner < 4; ++corner) + { + for (int seg = 0; seg < kSeg; ++seg) + { + float a0 = startA[corner] + kHalfPi * seg / kSeg; + float a1 = startA[corner] + kHalfPi * (seg + 1) / kSeg; + + float ox0 = cx_[corner] + cosf(a0) * radius; + float oy0 = cy_[corner] + sinf(a0) * radius; + float ox1 = cx_[corner] + cosf(a1) * radius; + float oy1 = cy_[corner] + sinf(a1) * radius; + float ix0 = cx_[corner] + cosf(a0) * ri; + float iy0 = cy_[corner] + sinf(a0) * ri; + float ix1 = cx_[corner] + cosf(a1) * ri; + float iy1 = cy_[corner] + sinf(a1) * ri; + + if (s_vertCount + 6 > kMaxVerts) FlushBatch(); + UIVertex* v = &s_vertices[s_vertCount]; + v[0] = { ox0, oy0, r, g, b, a, 0, 0 }; + v[1] = { ox1, oy1, r, g, b, a, 0, 0 }; + v[2] = { ix0, iy0, r, g, b, a, 0, 0 }; + v[3] = { ix0, iy0, r, g, b, a, 0, 0 }; + v[4] = { ox1, oy1, r, g, b, a, 0, 0 }; + v[5] = { ix1, iy1, r, g, b, a, 0, 0 }; + s_vertCount += 6; + } + } +} + +void DrawBorder(float x, float y, float w, float h, + float thickness, uint32_t color) +{ + DrawRect(x, y, w, thickness, color); + DrawRect(x, y + h - thickness, w, thickness, color); + DrawRect(x, y + thickness, thickness, h - 2.0f * thickness, color); + DrawRect(x + w - thickness, y + thickness, thickness, h - 2.0f * thickness, color); +} + +void DrawLine(float x, float y, float length, float thickness, uint32_t color) +{ + DrawRect(x, y, length, thickness, color); +} + +void DrawLineV(float x, float y, float length, float thickness, uint32_t color) +{ + DrawRect(x, y, thickness, length, color); +} + +void DrawGradientRect(float x, float y, float w, float h, + uint32_t topColor, uint32_t bottomColor) +{ + if (!s_inFrame) return; + EnsureBatchMode(BATCH_COLOR); + + float tr, tg, tb, ta, br, bg, bb, ba; + DecodeColor(topColor, tr, tg, tb, ta); + DecodeColor(bottomColor, br, bg, bb, ba); + PushGradientQuad(x, y, x + w, y + h, tr, tg, tb, ta, br, bg, bb, ba); +} + +void DrawGradientRoundedRect(float x, float y, float w, float h, + float radius, + uint32_t topColor, uint32_t bottomColor) +{ + if (!s_inFrame) return; + EnsureBatchMode(BATCH_COLOR); + + float maxR = fminf(w, h) * 0.5f; + if (radius > maxR) radius = maxR; + + float tr, tg, tb, ta, br, bg, bb, ba; + DecodeColor(topColor, tr, tg, tb, ta); + DecodeColor(bottomColor, br, bg, bb, ba); + + PushGradientQuad(x + radius, y, x + w - radius, y + h, + tr, tg, tb, ta, br, bg, bb, ba); + + float midTopY = y + radius; + float midBotY = y + h - radius; + PushGradientQuad(x, midTopY, x + radius, midBotY, + tr, tg, tb, ta, br, bg, bb, ba); + PushGradientQuad(x + w - radius, midTopY, x + w, midBotY, + tr, tg, tb, ta, br, bg, bb, ba); + + static constexpr float kHalfPi = 1.5707963f; + + float cx_[4] = { x + radius, x + w - radius, x + radius, x + w - radius }; + float cy_[4] = { y + radius, y + radius, y + h - radius, y + h - radius }; + float startA[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 }; + float cr[4] = { tr, tr, br, br }; + float cg[4] = { tg, tg, bg, bg }; + float cb[4] = { tb, tb, bb, bb }; + float ca[4] = { ta, ta, ba, ba }; + + EmitCornerFans(cx_, cy_, startA, radius, cr, cg, cb, ca); +} + +void DrawDropShadow(float x, float y, float w, float h, + float offset, float spread, uint32_t color) +{ + if (!s_inFrame) return; + + float r, g, b, a; + DecodeColor(color, r, g, b, a); + + int layers = 3; + for (int i = 0; i < layers; ++i) + { + float t = (float)(i + 1) / (float)layers; + float expand = spread * t; + float off = offset * t; + float layerA = a * (1.0f - t * 0.6f) / (float)layers; + + DrawRect(x - expand + off, y - expand + off, + w + expand * 2, h + expand * 2, + ((uint32_t)(layerA * 255.0f) << 24) | (color & 0x00FFFFFFu)); + } +} + +void DrawPanel(float x, float y, float w, float h, + float radius, uint32_t bgColor, uint32_t borderColor, + float borderThick, bool shadow) +{ + if (shadow) + DrawDropShadow(x, y, w, h, 4.0f, 6.0f, 0x60000000u); + + if (radius > 0.0f) + { + DrawRoundedRect(x, y, w, h, radius, bgColor); + if (borderThick > 0.0f) + DrawRoundedBorder(x, y, w, h, radius, borderThick, borderColor); + } + else + { + DrawRect(x, y, w, h, bgColor); + if (borderThick > 0.0f) + DrawBorder(x, y, w, h, borderThick, borderColor); + } +} + +void DrawDivider(float x, float y, float w, uint32_t color, float thickness) +{ + DrawRect(x, y, w, thickness, color); +} + +void DrawText(float x, float y, const wchar_t* text, + uint32_t color, float size, uint32_t align) +{ + if (!text || !text[0] || !s_inFrame) return; + Font* font = GetFont(); + if (!font) return; + + std::wstring wstr = font->sanitize(std::wstring(text)); + const float scale = ScaleForSize(size); + + float drawX, drawY; + ApplyAlignment(font, wstr, x, y, scale, align, drawX, drawY); + + if ((color & 0xFC000000) == 0) color |= 0xFF000000; + float r, g, b, a; + DecodeColor(color, r, g, b, a); + DrawGlyphString(font, wstr, drawX, drawY, scale, r, g, b, a); +} + +void DrawShadowText(float x, float y, const wchar_t* text, + uint32_t color, float size, uint32_t align) +{ + if (!text || !text[0] || !s_inFrame) return; + Font* font = GetFont(); + if (!font) return; + + std::wstring wstr = font->sanitize(std::wstring(text)); + const float scale = ScaleForSize(size); + + float drawX, drawY; + ApplyAlignment(font, wstr, x, y, scale, align, drawX, drawY); + + if ((color & 0xFC000000) == 0) color |= 0xFF000000; + + uint32_t shadow = (color & 0xfcfcfc) >> 2 | (color & 0xFF000000); + float sr, sg, sb, sa; + DecodeColor(shadow, sr, sg, sb, sa); + DrawGlyphString(font, wstr, drawX + scale, drawY + scale, scale, sr, sg, sb, sa); + + float r, g, b, a; + DecodeColor(color, r, g, b, a); + DrawGlyphString(font, wstr, drawX, drawY, scale, r, g, b, a); +} + +float DrawTextWrapped(float x, float y, const wchar_t* text, + float maxWidth, uint32_t color, + float size, uint32_t align) +{ + if (!text || !text[0] || !s_inFrame) return 0.0f; + Font* font = GetFont(); + if (!font) return 0.0f; + + std::wstring wstr = font->sanitize(std::wstring(text)); + const float scale = ScaleForSize(size); + const float lineH = LineHeight(size); + + float curY = y; + size_t lineStart = 0; + size_t lastSpace = std::wstring::npos; + + for (size_t i = 0; i <= wstr.length(); ++i) + { + bool endOfString = (i == wstr.length()); + bool isSpace = !endOfString && wstr[i] == L' '; + + if (isSpace) lastSpace = i; + + std::wstring segment = wstr.substr(lineStart, i - lineStart); + float segW = static_cast(font->width(segment)) * scale; + + if (segW > maxWidth || endOfString) + { + size_t breakAt; + if (endOfString) + breakAt = i; + else if (lastSpace != std::wstring::npos && lastSpace > lineStart) + breakAt = lastSpace; + else + breakAt = i > lineStart ? i - 1 : i; + + std::wstring line = wstr.substr(lineStart, breakAt - lineStart); + if (!line.empty()) + { + float drawX, drawY; + ApplyAlignment(font, line, x, curY, scale, align, drawX, drawY); + + if ((color & 0xFC000000) == 0) color |= 0xFF000000; + float r, g, b, a; + DecodeColor(color, r, g, b, a); + DrawGlyphString(font, line, drawX, drawY, scale, r, g, b, a); + + curY += lineH; + } + + lineStart = breakAt; + if (lineStart < wstr.length() && wstr[lineStart] == L' ') + ++lineStart; + lastSpace = std::wstring::npos; + i = lineStart; + } + } + + return curY - y; +} + +void MeasureText(const wchar_t* text, float size, + float* outWidth, float* outHeight) +{ + if (!text || !text[0]) + { + if (outWidth) *outWidth = 0.0f; + if (outHeight) *outHeight = 0.0f; + return; + } + Font* font = GetFont(); + if (!font) + { + if (outWidth) *outWidth = 0.0f; + if (outHeight) *outHeight = 0.0f; + return; + } + const float scale = ScaleForSize(size); + if (outWidth) *outWidth = static_cast(font->width(std::wstring(text))) * scale; + if (outHeight) *outHeight = size; +} + +float LineHeight(float size) +{ + return size + 2.0f * ScaleForSize(size); +} + +void PushClipRect(float x, float y, float w, float h) +{ + if (!s_inFrame) return; + FlushBatch(); + + ClipRect newClip = { x, y, w, h }; + + if (s_clipDepth > 0) + { + const ClipRect& parent = s_clipStack[s_clipDepth - 1]; + float nx0 = fmaxf(x, parent.x); + float ny0 = fmaxf(y, parent.y); + float nx1 = fminf(x + w, parent.x + parent.w); + float ny1 = fminf(y + h, parent.y + parent.h); + newClip.x = nx0; + newClip.y = ny0; + newClip.w = fmaxf(0.0f, nx1 - nx0); + newClip.h = fmaxf(0.0f, ny1 - ny0); + } + + if (s_clipDepth < kMaxClipStack) + s_clipStack[s_clipDepth++] = newClip; + + ApplyScissor(); +} + +void PopClipRect() +{ + if (!s_inFrame) return; + FlushBatch(); + + if (s_clipDepth > 0) --s_clipDepth; + ApplyScissor(); +} + +static int s_guiTexId = -2; + +static int GetGuiTexture() +{ + if (s_guiTexId == -2) + s_guiTexId = LoadTexture(L"/gui/gui.png"); + return s_guiTexId; +} + +static void DrawGuiSlice(float x, float y, float w, float h, + int texId, float srcY, float srcH, + float capL = 2.0f, float capR = 2.0f, + float capT = 2.0f, float capB = 3.0f) +{ + const float texSz = 256.0f; + const float srcW = 200.0f; + + float scale = h / srcH; + float cL = capL * scale; + float cR = capR * scale; + float cT = capT * scale; + float cB = capB * scale; + + if (cL + cR > w) { float s = w / (cL + cR); cL *= s; cR *= s; } + if (cT + cB > h) { float s = h / (cT + cB); cT *= s; cB *= s; } + + float midW = w - cL - cR; + float midH = h - cT - cB; + + float u0 = 0.0f; + float uL = capL / texSz; + float uR = (srcW - capR) / texSz; + float u1 = srcW / texSz; + + float v0 = srcY / texSz; + float vT = (srcY + capT) / texSz; + float vB = (srcY + srcH - capB) / texSz; + float v1 = (srcY + srcH) / texSz; + + DrawTextureUV(x, y, cL, cT, texId, u0, v0, uL, vT); + DrawTextureUV(x + cL, y, midW, cT, texId, uL, v0, uR, vT); + DrawTextureUV(x + w - cR, y, cR, cT, texId, uR, v0, u1, vT); + + DrawTextureUV(x, y + cT, cL, midH, texId, u0, vT, uL, vB); + DrawTextureUV(x + cL, y + cT, midW, midH, texId, uL, vT, uR, vB); + DrawTextureUV(x + w - cR, y + cT, cR, midH, texId, uR, vT, u1, vB); + + DrawTextureUV(x, y + h - cB, cL, cB, texId, u0, vB, uL, v1); + DrawTextureUV(x + cL, y + h - cB, midW, cB, texId, uL, vB, uR, v1); + DrawTextureUV(x + w - cR, y + h - cB, cR, cB, texId, uR, vB, u1, v1); +} + +void DrawButton(float x, float y, float w, float h, + const wchar_t* label, bool focused, bool hovered, + float labelSize) +{ + int texId = GetGuiTexture(); + + bool active = focused || hovered; + + if (texId >= 0) + { + float srcY = active ? 86.0f : 66.0f; + DrawGuiSlice(x, y, w, h, texId, srcY, 20.0f, 2.0f, 2.0f, 2.0f, 3.0f); + } + else + { + uint32_t bg = active ? 0xFF1B5A8Cu : 0xFF282828u; + DrawRoundedRect(x, y, w, h, 4.0f, bg); + } + + if (label && label[0]) + { + uint32_t textColor = active ? 0xFFFFFF55u : 0xFFFFFFFFu; + DrawShadowText(x + w * 0.5f, y + h * 0.5f, label, textColor, + labelSize, ALIGN_CENTER_X | ALIGN_CENTER_Y); + } +} + +static int s_enchantBtnTex = -2; +static int s_sliderTrackTex = -2; +static int s_sliderBtnTex = -2; + +void DrawTextBox(float x, float y, float w, float h, uint32_t tint) +{ + if (s_enchantBtnTex == -2) + s_enchantBtnTex = LoadTextureFromFile( + "Common/Media/Graphics/EnchantmentButtonEmpty.png"); + + if (s_enchantBtnTex >= 0) + { + float scale = h / 42.0f; + float capW = 6.0f * scale; + if (capW * 2 > w) capW = w * 0.5f; + float midW = w - capW * 2; + float uCap = 6.0f / 240.0f; + float uMid = (240.0f - 6.0f) / 240.0f; + + DrawTextureUV(x, y, capW, h, s_enchantBtnTex, 0.0f, 0.0f, uCap, 1.0f, tint); + DrawTextureUV(x + capW, y, midW, h, s_enchantBtnTex, uCap, 0.0f, uMid, 1.0f, tint); + DrawTextureUV(x + w - capW, y, capW, h, s_enchantBtnTex, uMid, 0.0f, 1.0f, 1.0f, tint); + } + else + { + DrawRoundedRect(x, y, w, h, 2.0f, 0xFF1A1A2Au); + DrawRoundedBorder(x, y, w, h, 2.0f, 1.0f, 0xFF444444u); + } +} + +void DrawProgressBar(float x, float y, float w, float h, + float progress, + uint32_t fillColor, uint32_t trackColor) +{ + float radius = h * 0.5f; + DrawRoundedRect(x, y, w, h, radius, trackColor); + float fill = w * (progress < 0.0f ? 0.0f : progress > 1.0f ? 1.0f : progress); + if (fill > radius * 2.0f) + DrawRoundedRect(x, y, fill, h, radius, fillColor); + else if (fill > 0.0f) + DrawRoundedRect(x, y, fmaxf(fill, h), h, radius, fillColor); +} + +void DrawSpinner(float cx, float cy, float radius, int tick, uint32_t color) +{ + static constexpr int kDots = 10; + static constexpr float kTwoPi = 6.28318530f; + + const uint32_t baseA = (color >> 24) & 0xFFu; + const uint32_t rgb = color & 0x00FFFFFFu; + + for (int i = 0; i < kDots; ++i) + { + const float angle = (float)i / kDots * kTwoPi - kTwoPi * 0.25f; + const float dx = cosf(angle) * radius; + const float dy = sinf(angle) * radius; + + const int age = (i - tick % kDots + kDots) % kDots; + const float t = 1.0f - (float)age / kDots; + const float dotR = radius * (0.10f + 0.12f * t); + const uint32_t a = (uint32_t)(baseA * (0.15f + 0.85f * t)) & 0xFFu; + + DrawRoundedRect(cx + dx - dotR, cy + dy - dotR, + dotR * 2.0f, dotR * 2.0f, dotR, (a << 24) | rgb); + } +} + +void DrawCheckbox(float x, float y, float size, bool checked, bool focused, + bool hovered) +{ + uint32_t bgColor, borderColor; + float radius = 3.0f; + + if (focused) + { + bgColor = 0xFF1A3A55u; + borderColor = 0xFF4DC3FFu; + } + else if (hovered) + { + bgColor = 0xFF2A3A48u; + borderColor = 0xFF3DA8E0u; + } + else + { + bgColor = 0xFF222222u; + borderColor = 0xFF555555u; + } + + DrawRoundedRect(x, y, size, size, radius, bgColor); + DrawRoundedBorder(x, y, size, size, radius, 2.0f, borderColor); + + if (checked) + { + float pad = size * 0.22f; + DrawRoundedRect(x + pad, y + pad, size - pad * 2, size - pad * 2, + 2.0f, 0xFF4DC3FFu); + } +} + +void DrawSlider(float x, float y, float w, float h, + float value, bool focused, bool hovered, + uint32_t fillColor, uint32_t trackColor) +{ + value = value < 0.0f ? 0.0f : value > 1.0f ? 1.0f : value; + + if (s_sliderTrackTex == -2) + s_sliderTrackTex = LoadTextureFromFile( + "Common/Media/Graphics/Slider_Track.png"); + if (s_sliderBtnTex == -2) + s_sliderBtnTex = LoadTextureFromFile( + "Common/Media/Graphics/Slider_Button.png"); + + if (s_sliderTrackTex >= 0) + { + float scale = h / 32.0f; + float capW = 8.0f * scale; + if (capW * 2 > w) capW = w * 0.5f; + float midW = w - capW * 2; + float uCap = 8.0f / 600.0f; + float uMid = (600.0f - 8.0f) / 600.0f; + + DrawTextureUV(x, y, capW, h, s_sliderTrackTex, 0.0f, 0.0f, uCap, 1.0f); + DrawTextureUV(x + capW, y, midW, h, s_sliderTrackTex, uCap, 0.0f, uMid, 1.0f); + DrawTextureUV(x + w - capW, y, capW, h, s_sliderTrackTex, uMid, 0.0f, 1.0f, 1.0f); + } + else + { + float radius = h * 0.5f; + DrawRoundedRect(x, y, w, h, radius, trackColor); + } + + if (s_sliderBtnTex >= 0) + { + float thumbH = h; + float thumbW = thumbH * (16.0f / 32.0f); + float thumbX = x + value * (w - thumbW); + if (thumbX < x) thumbX = x; + if (thumbX + thumbW > x + w) thumbX = x + w - thumbW; + + DrawTexture(thumbX, y, thumbW, thumbH, s_sliderBtnTex); + } + else + { + float thumbR = h * 0.8f; + float thumbX = x + w * value; + if (thumbX < x + thumbR) thumbX = x + thumbR; + if (thumbX > x + w - thumbR) thumbX = x + w - thumbR; + + uint32_t thumbColor = (focused || hovered) ? 0xFFFFFFFFu : 0xFFCCCCCCu; + DrawRoundedRect(thumbX - thumbR, y + h * 0.5f - thumbR, + thumbR * 2, thumbR * 2, thumbR, thumbColor); + } +} + +void DrawTooltip(float x, float y, const wchar_t* text, float size) +{ + if (!text || !text[0]) return; + + float tw, th; + MeasureText(text, size, &tw, &th); + + float padX = 10.0f, padY = 6.0f; + float boxW = tw + padX * 2; + float boxH = th + padY * 2; + float radius = 4.0f; + + float bx = x - boxW * 0.5f; + float by = y - boxH - 6.0f; + + DrawDropShadow(bx, by, boxW, boxH, 3.0f, 4.0f, 0x50000000u); + DrawRoundedRect(bx, by, boxW, boxH, radius, 0xF0181818u); + DrawRoundedBorder(bx, by, boxW, boxH, radius, 1.0f, 0xFF555555u); + DrawShadowText(x, by + padY, text, 0xFFFFFFFFu, size, ALIGN_CENTER_X); +} + +int LoadTexture(const wchar_t* path) +{ + if (!path || !path[0]) return -1; + Minecraft* mc = Minecraft::GetInstance(); + if (!mc || !mc->textures) return -1; + + return mc->textures->loadTextureByPath(std::wstring(path)); +} + +int LoadTextureByName(int textureName) +{ + Minecraft* mc = Minecraft::GetInstance(); + if (!mc || !mc->textures) return -1; + return mc->textures->loadTexture(textureName); +} + +int LoadTextureFromFile(const char* filePath, int* outWidth, int* outHeight) +{ + if (!filePath || !filePath[0]) return -1; + + std::string key(filePath); + auto it = s_fileTexCache.find(key); + if (it != s_fileTexCache.end()) + { + if (outWidth) *outWidth = it->second.w; + if (outHeight) *outHeight = it->second.h; + return it->second.id; + } + + D3DXIMAGE_INFO info; + ZeroMemory(&info, sizeof(info)); + int* pixelData = nullptr; + + HRESULT hr = RenderManager.LoadTextureData(filePath, &info, &pixelData); + if (FAILED(hr) || !pixelData) + return -1; + + int texId = RenderManager.TextureCreate(); + if (texId < 0) + { + free(pixelData); + return -1; + } + + RenderManager.TextureBind(texId); + RenderManager.TextureData(info.Width, info.Height, pixelData, 0); + free(pixelData); + + s_fileTexCache[key] = { texId, info.Width, info.Height }; + + if (outWidth) *outWidth = info.Width; + if (outHeight) *outHeight = info.Height; + return texId; +} + +int LoadTextureFromFileDirect(const char* filePath, int* outWidth, int* outHeight) +{ + if (!filePath || !filePath[0]) return -1; + + std::string key = std::string("$direct$") + filePath; + auto it = s_fileTexCache.find(key); + if (it != s_fileTexCache.end()) + { + if (outWidth) *outWidth = it->second.w; + if (outHeight) *outHeight = it->second.h; + return it->second.id; + } + + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + bool needUninit = SUCCEEDED(hr); + + IWICImagingFactory* factory = nullptr; + IWICBitmapDecoder* decoder = nullptr; + IWICBitmapFrameDecode*frame = nullptr; + IWICFormatConverter* converter = nullptr; + + auto cleanup = [&]() { + if (converter) converter->Release(); + if (frame) frame->Release(); + if (decoder) decoder->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 -1; } + + int wlen = MultiByteToWideChar(CP_UTF8, 0, filePath, -1, nullptr, 0); + std::vector wpath(wlen); + MultiByteToWideChar(CP_UTF8, 0, filePath, -1, wpath.data(), wlen); + + hr = factory->CreateDecoderFromFilename(wpath.data(), nullptr, + GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder); + if (FAILED(hr)) { cleanup(); return -1; } + + hr = decoder->GetFrame(0, &frame); + if (FAILED(hr)) { cleanup(); return -1; } + + UINT w = 0, h = 0; + frame->GetSize(&w, &h); + + hr = factory->CreateFormatConverter(&converter); + if (FAILED(hr)) { cleanup(); return -1; } + + converter->Initialize(frame, GUID_WICPixelFormat32bppRGBA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom); + + UINT stride = w * 4; + std::vector pixels(stride * h); + converter->CopyPixels(nullptr, stride, (UINT)pixels.size(), pixels.data()); + + cleanup(); + + int texId = RenderManager.TextureCreate(); + if (texId < 0) return -1; + + RenderManager.TextureBind(texId); + RenderManager.TextureData(w, h, pixels.data(), 0); + + s_fileTexCache[key] = { texId, (int)w, (int)h }; + if (outWidth) *outWidth = (int)w; + if (outHeight) *outHeight = (int)h; + return texId; +} + +NineSlice LoadNineSlice(const char* basePath) +{ + NineSlice ns = {}; + ns.valid = false; + if (!basePath || !basePath[0]) return ns; + + std::string base(basePath); + int cw = 0, ch = 0; + + ns.tl = LoadTextureFromFile((base + "_TL.png").c_str(), &cw, &ch); + + if (ns.tl >= 0) + { + ns.tm = LoadTextureFromFile((base + "_TM.png").c_str()); + ns.tr = LoadTextureFromFile((base + "_TR.png").c_str()); + ns.ml = LoadTextureFromFile((base + "_ML.png").c_str()); + ns.mm = LoadTextureFromFile((base + "_MM.png").c_str()); + ns.mr = LoadTextureFromFile((base + "_MR.png").c_str()); + ns.bl = LoadTextureFromFile((base + "_BL.png").c_str()); + ns.bm = LoadTextureFromFile((base + "_BM.png").c_str()); + ns.br = LoadTextureFromFile((base + "_BR.png").c_str()); + } + else + { + ns.tl = LoadTextureFromFile((base + "_Top_L.png").c_str(), &cw, &ch); + ns.tm = LoadTextureFromFile((base + "_Top_M.png").c_str()); + ns.tr = LoadTextureFromFile((base + "_Top_R.png").c_str()); + ns.ml = LoadTextureFromFile((base + "_Mid_L.png").c_str()); + ns.mm = LoadTextureFromFile((base + "_Mid_M.png").c_str()); + ns.mr = LoadTextureFromFile((base + "_Mid_R.png").c_str()); + ns.bl = LoadTextureFromFile((base + "_Bot_L.png").c_str()); + ns.bm = LoadTextureFromFile((base + "_Bot_M.png").c_str()); + ns.br = LoadTextureFromFile((base + "_Bot_R.png").c_str()); + } + + ns.cornerW = cw; + ns.cornerH = ch; + + ns.valid = (ns.tl >= 0 && ns.tm >= 0 && ns.tr >= 0 && + ns.ml >= 0 && ns.mm >= 0 && ns.mr >= 0 && + ns.bl >= 0 && ns.bm >= 0 && ns.br >= 0 && + cw > 0 && ch > 0); + + return ns; +} + +ThreeSlice LoadThreeSlice(const char* basePath) +{ + ThreeSlice ts = {}; + ts.valid = false; + if (!basePath || !basePath[0]) return ts; + + std::string base(basePath); + int cw = 0, ch = 0; + + ts.left = LoadTextureFromFile((base + "_Left.png").c_str(), &cw, &ch); + ts.mid = LoadTextureFromFile((base + "_Middle.png").c_str()); + ts.right = LoadTextureFromFile((base + "_Right.png").c_str()); + + ts.capW = cw; + ts.capH = ch; + + ts.valid = (ts.left >= 0 && ts.mid >= 0 && ts.right >= 0 && + cw > 0 && ch > 0); + + return ts; +} + +void DrawNineSlice(float x, float y, float w, float h, + const NineSlice& ns, uint32_t tint) +{ + if (!ns.valid || !s_inFrame) return; + + float cw = (float)ns.cornerW; + float ch = (float)ns.cornerH; + + if (cw * 2 > w) cw = w * 0.5f; + if (ch * 2 > h) ch = h * 0.5f; + + float midW = w - cw * 2; + float midH = h - ch * 2; + + DrawTexture(x, y, cw, ch, ns.tl, tint); + DrawTexture(x + cw, y, midW, ch, ns.tm, tint); + DrawTexture(x + w - cw, y, cw, ch, ns.tr, tint); + + DrawTexture(x, y + ch, cw, midH, ns.ml, tint); + DrawTexture(x + cw, y + ch, midW, midH, ns.mm, tint); + DrawTexture(x + w - cw, y + ch, cw, midH, ns.mr, tint); + + DrawTexture(x, y + h - ch, cw, ch, ns.bl, tint); + DrawTexture(x + cw, y + h - ch, midW, ch, ns.bm, tint); + DrawTexture(x + w - cw, y + h - ch, cw, ch, ns.br, tint); +} + +void DrawThreeSlice(float x, float y, float w, float h, + const ThreeSlice& ts, uint32_t tint) +{ + if (!ts.valid || !s_inFrame) return; + + float capW = (float)ts.capW; + + if (capW * 2 > w) capW = w * 0.5f; + + float midW = w - capW * 2; + + DrawTexture(x, y, capW, h, ts.left, tint); + DrawTexture(x + capW, y, midW, h, ts.mid, tint); + DrawTexture(x + w - capW, y, capW, h, ts.right, tint); +} + +void DrawTexture(float x, float y, float w, float h, + int textureId, uint32_t tint) +{ + DrawTextureUV(x, y, w, h, textureId, 0.0f, 0.0f, 1.0f, 1.0f, tint); +} + +void DrawTextureUV(float x, float y, float w, float h, + int textureId, + float u0, float v0, float u1, float v1, + uint32_t tint) +{ + if (!s_inFrame || textureId < 0) return; + + EnsureBatchMode(BATCH_TEXTURE, textureId); + + float r, g, b, a; + DecodeColor(tint, r, g, b, a); + PushQuad(x, y, x + w, y + h, r, g, b, a, u0, v0, u1, v1); +} + +void DrawTextureRounded(float x, float y, float w, float h, + int textureId, float radius, uint32_t tint) +{ + if (!s_inFrame || textureId < 0) return; + + PushClipRect(x + radius, y, w - radius * 2, h); + DrawTexture(x, y, w, h, textureId, tint); + PopClipRect(); + + PushClipRect(x, y + radius, radius, h - radius * 2); + DrawTexture(x, y, w, h, textureId, tint); + PopClipRect(); + + PushClipRect(x + w - radius, y + radius, radius, h - radius * 2); + DrawTexture(x, y, w, h, textureId, tint); + PopClipRect(); + + static constexpr int kSteps = 8; + static constexpr float kHalfPi = 1.5707963f; + + float cx_[4] = { x + radius, x + w - radius, x + radius, x + w - radius }; + float cy_[4] = { y + radius, y + radius, y + h - radius, y + h - radius }; + float startA[4] = { kHalfPi * 2, kHalfPi * 3, kHalfPi, 0 }; + + for (int corner = 0; corner < 4; ++corner) + { + for (int step = 0; step < kSteps; ++step) + { + float a0 = startA[corner] + kHalfPi * step / kSteps; + float a1 = startA[corner] + kHalfPi * (step + 1) / kSteps; + + float px0 = cx_[corner] + fminf(0.0f, fminf(cosf(a0), cosf(a1))) * radius; + float py0 = cy_[corner] + fminf(0.0f, fminf(sinf(a0), sinf(a1))) * radius; + float px1 = cx_[corner] + fmaxf(0.0f, fmaxf(cosf(a0), cosf(a1))) * radius; + float py1 = cy_[corner] + fmaxf(0.0f, fmaxf(sinf(a0), sinf(a1))) * radius; + + if (px1 > px0 && py1 > py0) + { + PushClipRect(px0, py0, px1 - px0, py1 - py0); + DrawTexture(x, y, w, h, textureId, tint); + PopClipRect(); + } + } + } +} + +void DrawTextureFit(float x, float y, float w, float h, + int textureId, int texW, int texH, + uint32_t tint) +{ + if (!s_inFrame || textureId < 0 || texW <= 0 || texH <= 0) return; + + float aspect = (float)texW / (float)texH; + float boxAspect = w / h; + + float drawW, drawH; + if (aspect > boxAspect) + { + drawW = w; + drawH = w / aspect; + } + else + { + drawH = h; + drawW = h * aspect; + } + + float drawX = x + (w - drawW) * 0.5f; + float drawY = y + (h - drawH) * 0.5f; + + DrawTexture(drawX, drawY, drawW, drawH, textureId, tint); +} + +bool NativeUI::GetMouseVirtual(float& outX, float& outY) +{ +#ifdef _WINDOWS64 + if (!g_hWnd) return false; + RECT rc; + GetClientRect(g_hWnd, &rc); + int winW = rc.right - rc.left; + int winH = rc.bottom - rc.top; + if (winW <= 0 || winH <= 0) return false; + + float pixX = (float)g_KBMInput.GetMouseX() / (float)winW * s_bbWidth; + float pixY = (float)g_KBMInput.GetMouseY() / (float)winH * s_bbHeight; + + outX = (pixX - s_vpX) / s_vpW * kVW; + outY = (pixY - s_vpY) / s_vpH * kVH; + return true; +#else + return false; +#endif +} + +void FocusList::Add(int id, float x, float y, float w, float h) +{ + m_entries.push_back({ id, x, y, w, h }); + if (m_focusIdx >= (int)m_entries.size()) + m_focusIdx = 0; +} + +int FocusList::GetFocused() const +{ + if (m_entries.empty()) return -1; + return m_entries[m_focusIdx].id; +} + +void FocusList::MoveNext() +{ + if (m_entries.empty()) return; + m_focusIdx = (m_focusIdx + 1) % (int)m_entries.size(); + ui.PlayUISFX(eSFX_Focus); +} + +void FocusList::MovePrev() +{ + if (m_entries.empty()) return; + m_focusIdx = (m_focusIdx - 1 + (int)m_entries.size()) % (int)m_entries.size(); + ui.PlayUISFX(eSFX_Focus); +} + +void FocusList::SetFocus(int id) +{ + for (int i = 0; i < (int)m_entries.size(); ++i) + { + if (m_entries[i].id == id) + { + m_focusIdx = i; + return; + } + } +} + +bool FocusList::HitTest(float mx, float my, int& outId) const +{ + for (const Entry& e : m_entries) + { + if (mx >= e.x && mx <= e.x + e.w && + my >= e.y && my <= e.y + e.h) + { + outId = e.id; + return true; + } + } + return false; +} + +bool FocusList::UpdateHover(float mx, float my) +{ + int hitId; + if (HitTest(mx, my, hitId)) + { + m_hoveredId = hitId; + return true; + } + m_hoveredId = -1; + return false; +} + +void FocusList::TickMouse() +{ +#ifdef _WINDOWS64 + if (m_mouseConsumed && !g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT)) + m_mouseConsumed = false; + + float mx, my; + if (!NativeUI::GetMouseVirtual(mx, my)) + { + ClearHover(); + m_lastHoveredId = -1; + return; + } + + bool mouseMoved = (mx != m_lastMouseX || my != m_lastMouseY); + m_lastMouseX = mx; + m_lastMouseY = my; + + if (!mouseMoved && m_lastDevice == eDevice_Gamepad) + { + return; + } + + if (mouseMoved) + m_lastDevice = eDevice_Mouse; + + int prevHover = m_lastHoveredId; + UpdateHover(mx, my); + m_lastHoveredId = m_hoveredId; + + if (m_hoveredId >= 0 && m_hoveredId != prevHover) + ui.PlayUISFX(eSFX_Focus); +#endif +} + +int FocusList::HandleMenuKey(int key, int backId, + float panelX, float panelY, + float panelW, float panelH) +{ + switch (key) + { + case ACTION_MENU_UP: + case ACTION_MENU_LEFT: + m_lastDevice = eDevice_Gamepad; + ClearHover(); + MovePrev(); + return RESULT_NAVIGATED; + case ACTION_MENU_DOWN: + case ACTION_MENU_RIGHT: + m_lastDevice = eDevice_Gamepad; + ClearHover(); + MoveNext(); + return RESULT_NAVIGATED; + case ACTION_MENU_OK: + { +#ifdef _WINDOWS64 + if (m_lastDevice == eDevice_Mouse && + g_KBMInput.IsMouseButtonDown(KeyboardMouseInput::MOUSE_LEFT)) + { + float mx, my; + if (NativeUI::GetMouseVirtual(mx, my)) + { + int hitId; + if (HitTest(mx, my, hitId)) + { + SetFocus(hitId); + ui.PlayUISFX(eSFX_Press); + m_mouseConsumed = true; + return hitId; + } + if (mx >= panelX && mx <= panelX + panelW && + my >= panelY && my <= panelY + panelH) + { + m_mouseConsumed = true; + return RESULT_UNHANDLED; + } + } + } +#endif + m_lastDevice = eDevice_Gamepad; + ui.PlayUISFX(eSFX_Press); + return GetFocused(); + } + case ACTION_MENU_CANCEL: + ui.PlayUISFX(eSFX_Back); + return backId; + default: + return RESULT_UNHANDLED; + } +} + +void DrawLink(float x, float y, const wchar_t* text, + const char* /*url*/, bool focused, bool hovered, + float size, uint32_t align, + float* outX, float* outY, float* outW, float* outH) +{ + if (!text || !text[0] || !s_inFrame) return; + + float tw, th; + MeasureText(text, size, &tw, &th); + + float drawX = x, drawY = y; + if (align & ALIGN_CENTER_X) drawX = x - tw * 0.5f; + else if (align & ALIGN_RIGHT) drawX = x - tw; + if (align & ALIGN_CENTER_Y) drawY = y - th * 0.5f; + else if (align & ALIGN_BOTTOM) drawY = y - th; + + uint32_t color; + float padX = 6.0f, padY = 3.0f; + + if (focused || hovered) + { + color = 0xFFFFFF55u; + } + else + { + color = 0xFF4DC3FFu; + } + + DrawShadowText(x, y, text, color, size, align); + + float underlineY = drawY + th + 1.0f; + float underlineThick = (focused || hovered) ? 2.0f : 1.0f; + DrawRect(drawX, underlineY, tw, underlineThick, color); + + if (outX) *outX = drawX - padX; + if (outY) *outY = drawY - padY; + if (outW) *outW = tw + padX * 2; + if (outH) *outH = th + padY * 2 + 2; +} + +void OpenURL(const char* url) +{ + if (!url || !url[0]) return; +#ifdef _WINDOWS64 + ShellExecuteA(nullptr, "open", url, nullptr, nullptr, SW_SHOWNORMAL); +#endif +} + +} diff --git a/Minecraft.Client/Common/UI/NativeUIRenderer.h b/Minecraft.Client/Common/UI/NativeUIRenderer.h new file mode 100644 index 0000000000..f647c943e6 --- /dev/null +++ b/Minecraft.Client/Common/UI/NativeUIRenderer.h @@ -0,0 +1,219 @@ +#pragma once +#include +#include +#include + +namespace NativeUI +{ + enum : uint32_t + { + ALIGN_LEFT = 0x00u, + ALIGN_RIGHT = 0x01u, + ALIGN_CENTER_X = 0x02u, + ALIGN_CENTER_Y = 0x04u, + ALIGN_BOTTOM = 0x08u, + }; + + void BeginFrame(); + void EndFrame(); + void Shutdown(); + bool GetMouseVirtual(float& outX, float& outY); + void DrawRect(float x, float y, float w, float h, uint32_t color); + void DrawRectFullscreen(uint32_t color); + void DrawRoundedRect(float x, float y, float w, float h, + float radius, uint32_t color); + void DrawRoundedBorder(float x, float y, float w, float h, + float radius, float thickness, uint32_t color); + + void DrawBorder(float x, float y, float w, float h, + float thickness, uint32_t color); + + void DrawLine(float x, float y, float length, float thickness, + uint32_t color); + + void DrawLineV(float x, float y, float length, float thickness, + uint32_t color); + + void DrawGradientRect(float x, float y, float w, float h, + uint32_t topColor, uint32_t bottomColor); + + void DrawGradientRoundedRect(float x, float y, float w, float h, + float radius, + uint32_t topColor, uint32_t bottomColor); + + void DrawDropShadow(float x, float y, float w, float h, + float offset = 4.0f, float spread = 6.0f, + uint32_t color = 0x60000000u); + + 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); + + void DrawDivider(float x, float y, float w, + uint32_t color = 0xFF333333u, + float thickness = 1.0f); + + 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); + + void PushClipRect(float x, float y, float w, float h); + void PopClipRect(); + + void DrawButton(float x, float y, float w, float h, + const wchar_t* label, bool focused, + bool hovered = false, float labelSize = 16.0f); + + void DrawTextBox(float x, float y, float w, float h, + uint32_t tint = 0xFFFFFFFFu); + + 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); + + void OpenURL(const char* url); + + int LoadTexture(const wchar_t* path); + + int LoadTextureByName(int textureName); + + int LoadTextureFromFileDirect(const char* filePath, + int* outWidth = nullptr, int* outHeight = nullptr); + + int LoadTextureFromFile(const char* filePath, + int* outWidth = nullptr, int* outHeight = nullptr); + + struct NineSlice + { + int tl, tm, tr; + int ml, mm, mr; + int bl, bm, br; + int cornerW, cornerH; + bool valid; + }; + + NineSlice LoadNineSlice(const char* basePath); + + void DrawNineSlice(float x, float y, float w, float h, + const NineSlice& ns, uint32_t tint = 0xFFFFFFFFu); + + struct ThreeSlice + { + int left, mid, right; + int capW, capH; + bool valid; + }; + + ThreeSlice LoadThreeSlice(const char* basePath); + + void DrawThreeSlice(float x, float y, float w, float h, + const ThreeSlice& ts, uint32_t tint = 0xFFFFFFFFu); + + void DrawTexture(float x, float y, float w, float h, + int textureId, uint32_t tint = 0xFFFFFFFFu); + + void DrawTextureUV(float x, float y, float w, float h, + int textureId, + float u0, float v0, float u1, float v1, + uint32_t tint = 0xFFFFFFFFu); + + void DrawTextureRounded(float x, float y, float w, float h, + int textureId, float radius, + uint32_t tint = 0xFFFFFFFFu); + + void DrawTextureFit(float x, float y, float w, float h, + int textureId, int texW, int texH, + uint32_t tint = 0xFFFFFFFFu); + + 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); } + + bool ShowFocus(int id) const + { + if (m_lastDevice == eDevice_Mouse) + return m_hoveredId >= 0 && m_hoveredId == id; + 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(); } + + void TickMouse(); + + bool IsMouseConsumed() const { return m_mouseConsumed; } + + static constexpr int RESULT_NAVIGATED = -1; + static constexpr int RESULT_UNHANDLED = -2; + + int HandleMenuKey(int key, int backId = 0, + float panelX = 0, float panelY = 0, + float panelW = 0, float panelH = 0); + + enum ELastDevice { eDevice_None, eDevice_Gamepad, eDevice_Mouse }; + ELastDevice GetLastDevice() const { return m_lastDevice; } + + private: + std::vector m_entries; + int m_focusIdx = 0; + int m_hoveredId = -1; + int m_lastHoveredId = -1; + bool m_mouseConsumed = false; + ELastDevice m_lastDevice = eDevice_None; + float m_lastMouseX = -1.0f; + float m_lastMouseY = -1.0f; + }; +} diff --git a/Minecraft.Client/Common/UI/UI.h b/Minecraft.Client/Common/UI/UI.h index a7c416f8ec..f59d507660 100644 --- a/Minecraft.Client/Common/UI/UI.h +++ b/Minecraft.Client/Common/UI/UI.h @@ -67,6 +67,9 @@ #include "UIScene_Intro.h" #include "UIScene_SaveMessage.h" #include "UIScene_MainMenu.h" +#ifndef MINECRAFT_SERVER_BUILD +#include "UIScene_MSAuth.h" +#endif #include "UIScene_LoadMenu.h" #include "UIScene_JoinMenu.h" #include "UIScene_LoadOrJoinMenu.h" diff --git a/Minecraft.Client/Common/UI/UIEnums.h b/Minecraft.Client/Common/UI/UIEnums.h index 45aff87df7..e79108d5d7 100644 --- a/Minecraft.Client/Common/UI/UIEnums.h +++ b/Minecraft.Client/Common/UI/UIEnums.h @@ -114,6 +114,7 @@ enum EUIScene #ifndef _XBOX // Anything non-xbox should be added here. The ordering of scenes above is required for sentient reporting on xbox 360 to continue to be accurate + eUIScene_MSAuth, eUIComponent_Panorama, eUIComponent_Logo, eUIComponent_DebugUIConsole, diff --git a/Minecraft.Client/Common/UI/UILayer.cpp b/Minecraft.Client/Common/UI/UILayer.cpp index e1c388f54f..8351d481a8 100644 --- a/Minecraft.Client/Common/UI/UILayer.cpp +++ b/Minecraft.Client/Common/UI/UILayer.cpp @@ -367,6 +367,11 @@ bool UILayer::NavigateToScene(int iPad, EUIScene scene, void *initData) case eUIScene_MainMenu: newScene = new UIScene_MainMenu(iPad, initData, this); break; + case eUIScene_MSAuth: +#ifndef MINECRAFT_SERVER_BUILD + newScene = new UIScene_MSAuth(iPad, initData, this); +#endif + break; case eUIScene_LoadOrJoinMenu: newScene = new UIScene_LoadOrJoinMenu(iPad, initData, this); break; diff --git a/Minecraft.Client/Common/UI/UIScene.cpp b/Minecraft.Client/Common/UI/UIScene.cpp index 303897a7f1..cd3f39e48a 100644 --- a/Minecraft.Client/Common/UI/UIScene.cpp +++ b/Minecraft.Client/Common/UI/UIScene.cpp @@ -1000,7 +1000,7 @@ void UIScene::gainFocus() updateTooltips(); updateComponents(); - if(!m_bFocussedOnce) + if(swf && !m_bFocussedOnce) { IggyDataValue result; IggyDataValue value[1]; diff --git a/Minecraft.Client/Common/UI/UIScene_MSAuth.cpp b/Minecraft.Client/Common/UI/UIScene_MSAuth.cpp new file mode 100644 index 0000000000..e19b78a9a6 --- /dev/null +++ b/Minecraft.Client/Common/UI/UIScene_MSAuth.cpp @@ -0,0 +1,1719 @@ +#include "stdafx.h" +#include "UI.h" +#include "UIScene_MSAuth.h" +#include "NativeUIRenderer.h" +#include "../../../newauth/include/newauthManager.h" +#include "../../../newauth/include/newauth.h" +#include +#include +#include + +using namespace MSAuthUI; + +#ifdef _WINDOWS64 +#include "../../Windows64/KeyboardMouseInput.h" +#include "../../Minecraft.h" +extern KeyboardMouseInput g_KBMInput; +#endif + +// auto-close after about 3 sec (assuming 30fps) +static constexpr int kAutoCloseTicks = 90; + +// skin png is on disk, waiting for main thread to load the texture +static constexpr int TEXTURE_PENDING_LOAD = -3; + +// how many rows we show before scrolling +static constexpr int kMaxVisible = 7; + +// panel size (1280x720 virtual coords) +static constexpr float kPX = 290.0f, kPY = 85.0f, kPW = 700.0f, kPH = 550.0f; + +// row layout, made em taller for the skin heads +static constexpr float kRowH = 52.0f; +static constexpr float kRowGap = 4.0f; +static constexpr float kRowLeft = kPX + 20.0f; +static constexpr float kRowWidth = kPW - 40.0f; +static constexpr float kRemoveBtnW = 100.0f; +static constexpr float kAccountTextW = kRowWidth - kRemoveBtnW - 8.0f; + +// head icon positioning +static constexpr float kHeadSize = 36.0f; +static constexpr float kHeadPad = 8.0f; +static constexpr float kTextAfterHead = kHeadSize + kHeadPad * 2; + +// list area starts below the title line +static constexpr float kListTop = kPY + 54.0f; +static constexpr float kListH = kMaxVisible * (kRowH + kRowGap) - kRowGap; + +// buttons at the bottom +static constexpr float kBtnY = kPY + kPH - 54.0f; +static constexpr float kBtnH = 40.0f; + +UIScene_MSAuth::UIScene_MSAuth(int iPad, void* initData, UILayer* parentLayer) + : UIScene(iPad, parentLayer) +{ + if (initData) + m_targetSlot = *reinterpret_cast(initData); + else + m_targetSlot = 0; + + // NOTE: do NOT call initialiseMovie() — this scene is a pure native + // overlay (NativeUIRenderer) with no Iggy/SWF movie. Calling + // initialiseMovie() would try to load a non-existent SWF and crash. + + SwitchToAccountList(); +} + +UIScene_MSAuth::~UIScene_MSAuth() +{ + // kill pending keyboard result so the callback doesnt write to dead memory + if (m_pendingKBResult) + m_pendingKBResult->valid.store(false, std::memory_order_release); +} + +// skin loading (download + cache) +static const char* kSkinCacheDir = "skins"; + +void UIScene_MSAuth::EnsureSkinLoaded(const std::string& uuid) +{ + if (uuid.empty()) return; + + // already cached? + auto it = m_skinCache.find(uuid); + if (it != m_skinCache.end()) { + auto& entry = *it->second; + // downloaded but not loaded yet, do it now on main thread + if (entry.textureId.load() == TEXTURE_PENDING_LOAD) { + int texId = NativeUI::LoadTextureFromFileDirect(entry.filePath.c_str()); + entry.textureId.store(texId >= 0 ? texId : -1); + } + return; + } + + // new cache entry, kick off download + auto entry = std::make_shared(); + entry->textureId.store(-1); // downloading + entry->filePath = std::string(kSkinCacheDir) + "/" + uuid + ".png"; + m_skinCache[uuid] = entry; + + // check disk cache first + struct stat st; + if (stat(entry->filePath.c_str(), &st) == 0 && st.st_size > 0) { + int texId = NativeUI::LoadTextureFromFileDirect(entry->filePath.c_str()); + entry->textureId.store(texId >= 0 ? texId : -1); + return; + } + + // figure out which auth provider this uuid uses + std::string skinProvider = "mojang"; + for (auto& acct : m_accounts) { + if (acct.uuid == uuid || newauth::UndashUuid(acct.uuid) == uuid) { + skinProvider = acct.authProvider; + break; + } + } + + // download in background + std::string uuidCopy = uuid; + std::thread([entry, uuidCopy, skinProvider]() { + try { + std::string error; + + std::string skinUrl; + if (skinProvider == "elyby") + skinUrl = newauth::ElybyFetchProfileSkinUrl(uuidCopy, error); + else + skinUrl = newauth::FetchProfileSkinUrl(uuidCopy, error); + if (skinUrl.empty()) { + entry->textureId.store(-1); + return; + } + + auto pngData = newauth::FetchSkinPngRaw(skinUrl, error); + if (pngData.empty()) { + entry->textureId.store(-1); + return; + } + + CreateDirectoryA(kSkinCacheDir, nullptr); + + std::ofstream f(entry->filePath, std::ios::binary); + if (!f) { + entry->textureId.store(-1); + return; + } + f.write(reinterpret_cast(pngData.data()), pngData.size()); + f.close(); + + // cant do d3d from a worker thread, tell main thread to load it + entry->textureId.store(TEXTURE_PENDING_LOAD); + } catch (...) { + // no internet or whatever, just mark it as failed and move on + entry->textureId.store(-1); + } + }).detach(); +} + +// view switching shit +void UIScene_MSAuth::SwitchToAccountList() +{ + m_view = eView_AccountList; + m_authFlags->done.store(false); + m_authFlags->success.store(false); + m_closeCountdown = 0; + m_scrollOffset = 0; + m_pendingRemoveIdx = -1; +} + +void UIScene_MSAuth::SwitchToDeviceCode() +{ + m_view = eView_DeviceCode; + // new flags so the old threads callback cant mess with this flow + m_authFlags = std::make_shared(); + m_closeCountdown = 0; + StartAddAccount(); +} + +static int MSAuthKeyboardCallback(LPVOID lpParam, bool bRes) +{ + // grab the shared_ptr off the heap and clean it up + auto* pShared = reinterpret_cast*>(lpParam); + auto pending = *pShared; + delete pShared; + if (bRes && pending && pending->valid.load(std::memory_order_acquire)) + { + uint16_t text[128]; + ZeroMemory(text, sizeof(text)); + Win64_GetKeyboardText(text, 128); + std::wstring ws(reinterpret_cast(text)); + // let all printable ascii through, validation happens in tick() + std::string result; + for (auto wc : ws) { + char c = (char)wc; + if (c >= 0x20 && c <= 0x7E) + result += c; + if (result.size() >= 128) break; + } + pending->value = result; + pending->ready.store(true, std::memory_order_release); + } + return 0; +} + +void UIScene_MSAuth::SwitchToOfflineInput() +{ + m_view = eView_OfflineInput; + m_offlineUsername.clear(); + m_offlineCursorBlink = 0; + m_textInputActive = false; +#ifdef _WINDOWS64 + g_KBMInput.ClearCharBuffer(); +#endif +} + +void UIScene_MSAuth::SwitchToElybyInput() +{ + m_view = eView_ElybyInput; + m_elybyUsername.clear(); + m_elybyPassword.clear(); + m_elyby2FACode.clear(); + m_authFlags->need2FA.store(false, std::memory_order_relaxed); + m_elybyActiveField = 0; + m_textInputActive = false; +#ifdef _WINDOWS64 + g_KBMInput.ClearCharBuffer(); +#endif +} + +void UIScene_MSAuth::SwitchToElyby2FA() +{ + m_view = eView_Elyby2FA; + m_elyby2FACode.clear(); + m_elybyActiveField = 2; + m_textInputActive = false; +#ifdef _WINDOWS64 + g_KBMInput.ClearCharBuffer(); +#endif +} + +void UIScene_MSAuth::SubmitElybyLogin() +{ + if (m_elybyUsername.empty() || m_elybyPassword.empty()) return; + m_textInputActive = false; + + auto flags = m_authFlags; + newauthManager::Get().BeginAddElybyAccount( + m_elybyUsername, m_elybyPassword, + [flags](bool ok, const newauth::JavaSession&, const std::string& error) { + if (error == "elyby_2fa_required") return; // 2fa callback handles this + flags->success.store(ok, std::memory_order_release); + flags->done.store(true, std::memory_order_release); + }, + [flags]() { + flags->need2FA.store(true, std::memory_order_release); + } + ); + m_view = eView_DeviceCode; // spinner time + m_closeCountdown = 0; +} + +void UIScene_MSAuth::SubmitElyby2FA() +{ + if (m_elyby2FACode.empty()) return; + m_textInputActive = false; + + // retry with the 2fa code appended + std::string combinedPassword = m_elybyPassword + ":" + m_elyby2FACode; + + auto flags = m_authFlags; + newauthManager::Get().BeginAddElybyAccount( + m_elybyUsername, combinedPassword, + [flags](bool ok, const newauth::JavaSession&, const std::string&) { + flags->success.store(ok, std::memory_order_release); + flags->done.store(true, std::memory_order_release); + } + ); + m_view = eView_DeviceCode; // loading spinner + m_closeCountdown = 0; +} + +void UIScene_MSAuth::ConfirmOfflineAccount() +{ + if (m_offlineUsername.empty()) return; + int idx = newauthManager::Get().AddOfflineJavaAccount(m_offlineUsername); + if (idx >= 0) + newauthManager::Get().SaveJavaAccountIndex(); + SwitchToAccountList(); +} + +void UIScene_MSAuth::StartAddAccount() +{ + // capture by value so this shit outlives the scene + auto flags = m_authFlags; + newauthManager::Get().BeginAddJavaAccount( + nullptr, + [flags](bool ok, const newauth::JavaSession&, const std::string&) + { + flags->success.store(ok, std::memory_order_release); + flags->done.store(true, std::memory_order_release); + } + ); +} + + +void UIScene_MSAuth::tick() +{ + // No Iggy movie → base tick() never sets m_hasTickedOnce, so set it + // manually so render() and input handling work: + m_hasTickedOnce = true; + + UIScene::tick(); + + // Throttle animation counters to ~30 FPS regardless of actual frame rate + ULONGLONG now = GetTickCount64(); + if (now - m_lastAnimTime >= 33) // ~30 Hz + { + m_lastAnimTime = now; + ++m_spinnerTick; + ++m_offlineCursorBlink; + } + + if (m_inputGuardTicks > 0) + --m_inputGuardTicks; + + m_accounts = newauthManager::Get().GetJavaAccounts(); + m_activeIdx = newauthManager::Get().GetSlot(m_targetSlot).accountIndex; + + // start skin downloads for online accounts + for (auto& acct : m_accounts) { + if (!acct.isOffline && !acct.uuid.empty()) + EnsureSkinLoaded(acct.uuid); + } + + // grab virtual keyboard result if theres one + if (m_pendingKBResult && m_pendingKBResult->ready.load(std::memory_order_acquire)) + { + if (m_view == eView_ElybyInput) { + if (m_elybyActiveField == 0) + m_elybyUsername = m_pendingKBResult->value; + else + m_elybyPassword = m_pendingKBResult->value; + } else if (m_view == eView_Elyby2FA) { + m_elyby2FACode = m_pendingKBResult->value; + } else { + // filter to valid mc username chars, 16 max + std::string filtered; + for (char c : m_pendingKBResult->value) { + if (isalnum((unsigned char)c) || c == '_') + filtered += c; + if (filtered.size() >= 16) break; + } + m_offlineUsername = filtered; + } + m_pendingKBResult.reset(); + } + + if (m_view == eView_OfflineInput) + { + + // unfocus text box if we tabbed away + if (m_textInputActive && !m_focus.IsActive(eBtn_OfflineTextBox)) + m_textInputActive = false; + +#ifdef _WINDOWS64 + // only eat keyboard when text box is focused + if (m_textInputActive) + { + wchar_t ch; + while (g_KBMInput.ConsumeChar(ch)) + { + if (ch == 0x08) // backspace + { + if (!m_offlineUsername.empty()) + m_offlineUsername.pop_back(); + } + else if (ch == 0x0D) // enter — confirm and deactivate + { + m_textInputActive = false; + if (!m_offlineUsername.empty()) + ConfirmOfflineAccount(); + } + else if (ch == 0x1B) // escape — deactivate text input + { + m_textInputActive = false; + } + else if (m_offlineUsername.size() < 16) + { + char c = (char)ch; + if (isalnum((unsigned char)c) || c == '_') + m_offlineUsername += c; + } + } + } + else + { + // flush stale chars so they dont pop up later + wchar_t ch; + while (g_KBMInput.ConsumeChar(ch)) {} + } +#endif + } + + // check if ely.by wants 2fa + if (m_authFlags->need2FA.load(std::memory_order_acquire)) + { + m_authFlags->need2FA.store(false, std::memory_order_relaxed); + SwitchToElyby2FA(); + } + + if (m_view == eView_ElybyInput || m_view == eView_Elyby2FA) + { + ++m_offlineCursorBlink; + + // which text field are we typing in + int activeBtn = -1; + if (m_view == eView_ElybyInput) { + if (m_elybyActiveField == 0) activeBtn = eBtn_ElybyUsername; + else if (m_elybyActiveField == 1) activeBtn = eBtn_ElybyPassword; + } else { + activeBtn = eBtn_Elyby2FACode; + } + + if (m_textInputActive && activeBtn >= 0 && !m_focus.IsActive(activeBtn)) + m_textInputActive = false; + +#ifdef _WINDOWS64 + if (m_textInputActive) + { + std::string* targetStr = nullptr; + size_t maxLen = 64; + bool allowAll = true; // passwords can have anything + if (m_view == eView_ElybyInput && m_elybyActiveField == 0) { + targetStr = &m_elybyUsername; maxLen = 64; allowAll = true; + } else if (m_view == eView_ElybyInput && m_elybyActiveField == 1) { + targetStr = &m_elybyPassword; maxLen = 128; allowAll = true; + } else if (m_view == eView_Elyby2FA) { + targetStr = &m_elyby2FACode; maxLen = 10; allowAll = false; + } + + if (targetStr) + { + wchar_t ch; + while (g_KBMInput.ConsumeChar(ch)) + { + if (ch == 0x08) { // backspace + if (!targetStr->empty()) targetStr->pop_back(); + } + else if (ch == 0x09) { // tab — move to next field + m_textInputActive = false; + if (m_view == eView_ElybyInput) { + m_elybyActiveField = (m_elybyActiveField + 1) % 2; + m_textInputActive = true; + g_KBMInput.ClearCharBuffer(); + } + } + else if (ch == 0x0D) { // enter + m_textInputActive = false; + if (m_view == eView_ElybyInput) + SubmitElybyLogin(); + else + SubmitElyby2FA(); + } + else if (ch == 0x1B) { + m_textInputActive = false; + } + else if (targetStr->size() < maxLen) { + char c = (char)ch; + if (allowAll && c >= 0x20 && c <= 0x7E) + *targetStr += c; + else if (!allowAll && c >= '0' && c <= '9') + *targetStr += c; + } + } + } + } + else + { + wchar_t ch; + while (g_KBMInput.ConsumeChar(ch)) {} + } +#endif + } + + if (m_view == eView_DeviceCode) + { + if (m_authFlags->done.load(std::memory_order_acquire)) + { + m_authFlags->done.store(false, std::memory_order_relaxed); + m_closeCountdown = kAutoCloseTicks; + } + + if (m_closeCountdown > 0) + { + if (--m_closeCountdown <= 0) + SwitchToAccountList(); + } + + m_cachedUri = newauthManager::Get().GetJavaDirectUri(); + } + + // build focus list + int prevFocus = m_focus.GetFocused(); + m_focus.Clear(); + + if (m_view == eView_AccountList) + { + if (m_pendingRemoveIdx >= 0) + { + // confirm dialog, no is default because we're not monsters + float dlgW = 360.0f, dlgH = 160.0f; + float dlgX = 640.0f - dlgW * 0.5f, dlgY = 360.0f - dlgH * 0.5f; + float dbtnW = 140.0f, dbtnH = 36.0f; + float dbtnY = dlgY + dlgH - 48.0f; + float gap = 16.0f; + float dbtnStartX = dlgX + (dlgW - dbtnW * 2 - gap) * 0.5f; + // no first, yes second + m_focus.Add(eBtn_ConfirmNo, dbtnStartX, dbtnY, dbtnW, dbtnH); + m_focus.Add(eBtn_ConfirmYes, dbtnStartX + dbtnW + gap, dbtnY, dbtnW, dbtnH); + } + else + { + int visible = (int)m_accounts.size() - m_scrollOffset; + if (visible > kMaxVisible) visible = kMaxVisible; + + for (int i = 0; i < visible; ++i) + { + float y = kListTop + i * (kRowH + kRowGap); + m_focus.Add(eAccountBase + m_scrollOffset + i, kRowLeft, y, kAccountTextW, kRowH); + m_focus.Add(eBtn_RemoveBase + m_scrollOffset + i, + kRowLeft + kAccountTextW + 8.0f, y, kRemoveBtnW, kRowH); + } + + // bottom buttons + float totalBtnW = 160.0f + 8.0f + 120.0f + 8.0f + 160.0f + 8.0f + 140.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + m_focus.Add(eBtn_AddAccount, btnStartX, kBtnY, 160.0f, kBtnH); + m_focus.Add(eBtn_AddElyby, btnStartX + 168.0f, kBtnY, 120.0f, kBtnH); + m_focus.Add(eBtn_AddOffline, btnStartX + 296.0f, kBtnY, 160.0f, kBtnH); + m_focus.Add(eBtn_Back, btnStartX + 464.0f, kBtnY, 140.0f, kBtnH); + } + } + else if (m_view == eView_OfflineInput) + { + // text box + const float bx = kPX + 120.0f, by = kPY + 156.0f; + const float bw = kPW - 240.0f, bh = 52.0f; + m_focus.Add(eBtn_OfflineTextBox, bx, by, bw, bh); + + // buttons + float totalBtnW = 200.0f + 12.0f + 200.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + m_focus.Add(eBtn_OfflineConfirm, btnStartX, kBtnY, 200.0f, kBtnH); + m_focus.Add(eBtn_Back, btnStartX + 212.0f, kBtnY, 200.0f, kBtnH); + } + else if (m_view == eView_ElybyInput) + { + const float bx = kPX + 120.0f, bw = kPW - 240.0f, bh = 44.0f; + m_focus.Add(eBtn_ElybyUsername, bx, kPY + 130.0f, bw, bh); + m_focus.Add(eBtn_ElybyPassword, bx, kPY + 210.0f, bw, bh); + + float totalBtnW = 160.0f + 12.0f + 160.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + m_focus.Add(eBtn_ElybySignIn, btnStartX, kBtnY, 160.0f, kBtnH); + m_focus.Add(eBtn_ElybyCancel, btnStartX + 172.0f, kBtnY, 160.0f, kBtnH); + } + else if (m_view == eView_Elyby2FA) + { + const float bx = kPX + 160.0f, bw = kPW - 320.0f, bh = 44.0f; + m_focus.Add(eBtn_Elyby2FACode, bx, kPY + 160.0f, bw, bh); + + float totalBtnW = 160.0f + 12.0f + 160.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + m_focus.Add(eBtn_Elyby2FASubmit, btnStartX, kBtnY, 160.0f, kBtnH); + m_focus.Add(eBtn_Elyby2FACancel, btnStartX + 172.0f, kBtnY, 160.0f, kBtnH); + } + else // eView_DeviceCode + { + if (!m_cachedUri.empty()) + { + float tw = 0, th = 0; + std::wstring wuri(m_cachedUri.begin(), m_cachedUri.end()); + NativeUI::MeasureText(wuri.c_str(), 15.0f, &tw, &th); + float padX = 8.0f, padY = 4.0f; + float linkX = 640.0f - tw * 0.5f - padX; + float linkY = kPY + 128.0f - padY; + m_focus.Add(eLink_URL, linkX, linkY, tw + padX * 2, th + padY * 2 + 2); + } + m_focus.Add(eBtn_Back, kPX + (kPW - 200.0f) * 0.5f, kBtnY, 200.0f, kBtnH); + } + + if (prevFocus >= 0) + m_focus.SetFocus(prevFocus); + + m_focus.TickMouse(); +} + + +// draws a numbered circle for the steps + +static void DrawStepCircle(float cx, float cy, int number, bool completed) +{ + uint32_t circleColor = completed ? 0xFF55DD55u : 0xFF3C6E96u; + NativeUI::DrawRoundedRect(cx - 14, cy - 14, 28, 28, 14.0f, circleColor); + + wchar_t num[4]; + num[0] = L'0' + number; + num[1] = 0; + NativeUI::DrawShadowText(cx, cy, completed ? L"\u2713" : num, + 0xFFFFFFFFu, 14.0f, + NativeUI::ALIGN_CENTER_X | NativeUI::ALIGN_CENTER_Y); +} + + +// steve head placeholder + +static void DrawHeadPlaceholder(float x, float y, float size) +{ + // border + NativeUI::DrawRoundedRect(x, y, size, size, 3.0f, 0xFF2A2A2Au); + // fill + NativeUI::DrawRoundedRect(x + 2, y + 2, size - 4, size - 4, 2.0f, 0xFF4A3728u); + // eyes lol + float eyeSize = size * 0.12f; + float eyeY = y + size * 0.42f; + NativeUI::DrawRect(x + size * 0.25f, eyeY, eyeSize, eyeSize, 0xFFFFFFFFu); + NativeUI::DrawRect(x + size * 0.63f, eyeY, eyeSize, eyeSize, 0xFFFFFFFFu); +} + + +// render player head from actual skin texture + +static void DrawSkinHead(float x, float y, float size, int skinTexId) +{ + // face layer (8,8)-(16,16) + NativeUI::DrawTextureUV(x, y, size, size, skinTexId, + 8.0f / 64.0f, 8.0f / 64.0f, + 16.0f / 64.0f, 16.0f / 64.0f); + // hat overlay (40,8)-(48,16) + NativeUI::DrawTextureUV(x, y, size, size, skinTexId, + 40.0f / 64.0f, 8.0f / 64.0f, + 48.0f / 64.0f, 16.0f / 64.0f); +} + +// skin cache alias +using SkinCacheMap = std::unordered_map>; + + +// account list rendering + +// returns which player has this account, 0 if nobody +static int GetSlotUsingAccount(int selfSlot, int accountIndex) +{ + for (int i = 0; i < XUSER_MAX_COUNT; ++i) { + if (i == selfSlot) continue; + if (newauthManager::Get().GetSlot(i).accountIndex == accountIndex) + return i + 1; // player number (1-based) + } + return 0; +} + +static void RenderAccountList(const NativeUI::NineSlice& panel, + const NativeUI::NineSlice& recessPanel, + const std::vector& accounts, + int activeIdx, int scrollOffset, int spinnerTick, + NativeUI::FocusList& focus, + const SkinCacheMap& skinCache, + int pendingRemoveIdx, + int targetSlot) +{ + // main panel + if (panel.valid) + NativeUI::DrawNineSlice(kPX, kPY, kPW, kPH, panel); + else + NativeUI::DrawPanel(kPX, kPY, kPW, kPH); + + // title + NativeUI::DrawShadowText(640.0f, kPY + 14.0f, L"Account Manager", + 0xFFFFFFFFu, 22.0f, NativeUI::ALIGN_CENTER_X); + + NativeUI::DrawDivider(kPX + 20.0f, kPY + 44.0f, kPW - 40.0f, 0x40FFFFFFu); + + // account list + if (accounts.empty()) + { + // nothing here yet + float centerY = kListTop + kListH * 0.35f; + NativeUI::DrawShadowText(640.0f, centerY, L"No accounts saved", + 0xFFAAAAAA, 18.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawShadowText(640.0f, centerY + 28.0f, + L"Press \"Add Account\" to sign in with Microsoft", + 0xFF777777u, 13.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawShadowText(640.0f, centerY + 48.0f, + L"or \"Add Offline\" to play without authentication.", + 0xFF777777u, 13.0f, NativeUI::ALIGN_CENTER_X); + } + else + { + int visible = (int)accounts.size() - scrollOffset; + if (visible > kMaxVisible) visible = kMaxVisible; + + // list bg + const float recessMargin = 6.0f; + float rX = kRowLeft - recessMargin; + float rY = kListTop - recessMargin; + float rW = kRowWidth + recessMargin * 2; + float rH = kListH + recessMargin * 2; + + if (recessPanel.valid) + NativeUI::DrawNineSlice(rX, rY, rW, rH, recessPanel); + else + NativeUI::DrawRoundedRect(rX, rY, rW, rH, 4.0f, 0x60000000u); + + // clip region so rows dont overflow + NativeUI::PushClipRect(kPX, kListTop, kPW, kListH); + + for (int i = 0; i < visible; ++i) + { + int idx = scrollOffset + i; + auto& acct = accounts[idx]; + float y = kListTop + i * (kRowH + kRowGap); + + bool isActive = (idx == activeIdx); + bool rowFocused = focus.ShowFocus(eAccountBase + idx); + bool rowHovered = focus.IsHovered(eAccountBase + idx); + + // row bg + uint32_t rowBg = 0x30000000u; + if (isActive) rowBg = 0x4000AA00u; + if (rowHovered) rowBg = 0x50FFFFFF; + if (rowFocused) rowBg = 0x60FFFFFF; + NativeUI::DrawRoundedRect(kRowLeft, y, kAccountTextW, kRowH, 4.0f, rowBg); + + // focus highlight + if (rowFocused || rowHovered) + { + uint32_t borderColor = rowFocused ? 0xAAFFFFFFu : 0x60FFFFFFu; + NativeUI::DrawRoundedBorder(kRowLeft, y, kAccountTextW, kRowH, + 4.0f, 1.0f, borderColor); + } + + // green bar = active account + if (isActive) + NativeUI::DrawRoundedRect(kRowLeft, y, 4.0f, kRowH, 2.0f, 0xFF55DD55u); + + // player head + float headX = kRowLeft + kHeadPad; + float headY = y + (kRowH - kHeadSize) * 0.5f; + bool headDrawn = false; + + if (!acct.isOffline && !acct.uuid.empty()) { + auto skinIt = skinCache.find(acct.uuid); + if (skinIt != skinCache.end()) { + int texId = skinIt->second->textureId.load(); + if (texId >= 0) { + DrawSkinHead(headX, headY, kHeadSize, texId); + headDrawn = true; + } + } + } + + if (!headDrawn) { + if (acct.isOffline) + DrawHeadPlaceholder(headX, headY, kHeadSize); + else { + // still loading, show a dark placeholder + NativeUI::DrawRoundedRect(headX, headY, kHeadSize, kHeadSize, 3.0f, 0xFF222222u); + } + } + + // username + float textX = kRowLeft + kTextAfterHead; + float textY = y + 8.0f; + std::wstring wname; + if (acct.username.empty()) + wname = L"(refreshing...)"; + else + wname = std::wstring(acct.username.begin(), acct.username.end()); + + uint32_t nameColor = (rowFocused || rowHovered) ? 0xFFFFFF55u : 0xFFFFFFFFu; + NativeUI::DrawShadowText(textX, textY, wname.c_str(), + nameColor, 16.0f, NativeUI::ALIGN_LEFT); + + // type badge + uuid line + float subY = textY + 21.0f; + + // type pill + { + const wchar_t* badge; + uint32_t pillBg, pillTxt; + if (acct.isOffline) { + badge = L"Offline"; pillBg = 0x50AA8800u; pillTxt = 0xFFCCBB55u; + } else if (acct.authProvider == "elyby") { + badge = L"Ely.by"; pillBg = 0x5000AAAAu; pillTxt = 0xFF77CCCCu; + } else { + badge = L"Microsoft"; pillBg = 0x5000AA00u; pillTxt = 0xFF77CC77u; + } + + float tw = 0, th = 0; + NativeUI::MeasureText(badge, 9.0f, &tw, &th); + NativeUI::DrawRoundedRect(textX, subY, tw + 8.0f, th + 4.0f, 3.0f, pillBg); + NativeUI::DrawText(textX + 4.0f, subY + 2.0f, badge, + pillTxt, 9.0f, NativeUI::ALIGN_LEFT); + + // uuid + if (!acct.uuid.empty()) + { + std::wstring wuuid(acct.uuid.begin(), acct.uuid.end()); + if (wuuid.size() > 13) + wuuid = wuuid.substr(0, 8) + L"..."; + NativeUI::DrawText(textX + tw + 16.0f, subY + 2.0f, wuuid.c_str(), + 0xFF666666u, 9.0f, NativeUI::ALIGN_LEFT); + } + } + + // active/player badge on the right side + int usedByPlayer = GetSlotUsingAccount(targetSlot, idx); + if (isActive) + { + float aw = 0, ah = 0; + NativeUI::MeasureText(L"ACTIVE", 10.0f, &aw, &ah); + float ax = kRowLeft + kAccountTextW - aw - 12.0f; + float ay = y + (kRowH - ah - 4.0f) * 0.5f; + NativeUI::DrawRoundedRect(ax, ay, aw + 8.0f, ah + 4.0f, 3.0f, 0x6055DD55u); + NativeUI::DrawText(ax + 4.0f, ay + 2.0f, L"ACTIVE", + 0xFF55DD55u, 10.0f, NativeUI::ALIGN_LEFT); + } + else if (usedByPlayer > 0) + { + wchar_t label[24]; + swprintf(label, 24, L"Player %d", usedByPlayer); + float aw = 0, ah = 0; + NativeUI::MeasureText(label, 10.0f, &aw, &ah); + float ax = kRowLeft + kAccountTextW - aw - 12.0f; + float ay = y + (kRowH - ah - 4.0f) * 0.5f; + NativeUI::DrawRoundedRect(ax, ay, aw + 8.0f, ah + 4.0f, 3.0f, 0x60DD8855u); + NativeUI::DrawText(ax + 4.0f, ay + 2.0f, label, + 0xFFDD8855u, 10.0f, NativeUI::ALIGN_LEFT); + } + + // remove btn + float rmX = kRowLeft + kAccountTextW + 8.0f; + bool rmFocused = focus.ShowFocus(eBtn_RemoveBase + idx); + bool rmHovered = focus.IsHovered(eBtn_RemoveBase + idx); + NativeUI::DrawButton(rmX, y + 6.0f, kRemoveBtnW, kRowH - 12.0f, L"Remove", + rmFocused, rmHovered, 12.0f); + } + + NativeUI::PopClipRect(); + + // scroll arrows + { + static int sTexUp = -2; + static int sTexDown = -2; + if (sTexUp == -2) sTexUp = NativeUI::LoadTextureFromFileDirect("Common/Media/Graphics/scrollUp.png"); + if (sTexDown == -2) sTexDown = NativeUI::LoadTextureFromFileDirect("Common/Media/Graphics/scrollDown.png"); + + // arrows at 1.2x scale + const float arrowW = 38.0f, arrowH = 26.0f; + const float gap = 4.0f; + const float arrowY = kListTop + kListH + 4.0f; + const float arrowX = kPX + kPW - 20.0f - arrowW * 2 - gap; + + bool canUp = scrollOffset > 0; + bool canDown = scrollOffset + kMaxVisible < (int)accounts.size(); + + if (canUp && sTexUp >= 0) + NativeUI::DrawTexture(arrowX, arrowY, arrowW, arrowH, sTexUp); + if (canDown && sTexDown >= 0) + NativeUI::DrawTexture(arrowX + arrowW + gap, arrowY, arrowW, arrowH, sTexDown); + } + } + + // divider + NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); + + // bottom buttons + float totalBtnW = 160.0f + 8.0f + 120.0f + 8.0f + 160.0f + 8.0f + 140.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + NativeUI::DrawButton(btnStartX, kBtnY, 160.0f, kBtnH, L"Microsoft", + focus.ShowFocus(eBtn_AddAccount), focus.IsHovered(eBtn_AddAccount)); + NativeUI::DrawButton(btnStartX + 168.0f, kBtnY, 120.0f, kBtnH, L"Ely.by", + focus.ShowFocus(eBtn_AddElyby), focus.IsHovered(eBtn_AddElyby)); + NativeUI::DrawButton(btnStartX + 296.0f, kBtnY, 160.0f, kBtnH, L"Offline", + focus.ShowFocus(eBtn_AddOffline), focus.IsHovered(eBtn_AddOffline)); + NativeUI::DrawButton(btnStartX + 464.0f, kBtnY, 140.0f, kBtnH, L"Done", + focus.ShowFocus(eBtn_Back), focus.IsHovered(eBtn_Back)); + + // remove confirm dialog + if (pendingRemoveIdx >= 0 && pendingRemoveIdx < (int)accounts.size()) + { + // dim the background + NativeUI::DrawRect(kPX, kPY, kPW, kPH, 0xA0000000u); + + // dialog panel + float dlgW = 400.0f, dlgH = 170.0f; + float dlgX = 640.0f - dlgW * 0.5f, dlgY = 360.0f - dlgH * 0.5f; + + if (panel.valid) + NativeUI::DrawNineSlice(dlgX, dlgY, dlgW, dlgH, panel); + else + NativeUI::DrawPanel(dlgX, dlgY, dlgW, dlgH); + + // title + NativeUI::DrawShadowText(640.0f, dlgY + 16.0f, L"Remove Account?", + 0xFFFFFFFFu, 18.0f, NativeUI::ALIGN_CENTER_X); + + NativeUI::DrawDivider(dlgX + 16.0f, dlgY + 42.0f, dlgW - 32.0f, 0x40FFFFFFu); + + // which account is getting nuked + std::wstring removeName(accounts[pendingRemoveIdx].username.begin(), + accounts[pendingRemoveIdx].username.end()); + if (removeName.empty()) removeName = L"(unknown)"; + + std::wstring removeMsg = L"\"" + removeName + L"\""; + NativeUI::DrawShadowText(640.0f, dlgY + 54.0f, removeMsg.c_str(), + 0xFFFFFF55u, 16.0f, NativeUI::ALIGN_CENTER_X); + + NativeUI::DrawShadowText(640.0f, dlgY + 78.0f, L"will be removed from this device.", + 0xFFFFFFFFu, 13.0f, NativeUI::ALIGN_CENTER_X); + + // cancel/remove buttons + float dbtnW = 160.0f, dbtnH = 36.0f; + float dbtnY = dlgY + dlgH - 52.0f; + float gap = 12.0f; + float dbtnStartX = dlgX + (dlgW - dbtnW * 2 - gap) * 0.5f; + + NativeUI::DrawButton(dbtnStartX, dbtnY, dbtnW, dbtnH, L"Cancel", + focus.ShowFocus(eBtn_ConfirmNo), focus.IsHovered(eBtn_ConfirmNo)); + NativeUI::DrawButton(dbtnStartX + dbtnW + gap, dbtnY, dbtnW, dbtnH, L"Remove", + focus.ShowFocus(eBtn_ConfirmYes), focus.IsHovered(eBtn_ConfirmYes)); + } +} + + +// device code view rendering + +static void RenderDeviceCode(const NativeUI::NineSlice& panel, + int spinnerTick, int closeCountdown, + bool authSuccess, + NativeUI::FocusList& focus, + newauthManager& auth) +{ + if (panel.valid) + NativeUI::DrawNineSlice(kPX, kPY, kPW, kPH, panel); + else + NativeUI::DrawPanel(kPX, kPY, kPW, kPH); + + NativeUI::DrawShadowText(640.0f, kPY + 14.0f, L"Sign In with Microsoft", + 0xFFFFFFFFu, 22.0f, NativeUI::ALIGN_CENTER_X); + + NativeUI::DrawDivider(kPX + 20.0f, kPY + 44.0f, kPW - 40.0f, 0x40FFFFFFu); + + auto state = auth.GetState(); + + // hell yeah it worked + if (state == newauthManager::State::Success && (closeCountdown > 0 || authSuccess)) + { + // big checkmark + NativeUI::DrawRoundedRect(640.0f - 30, kPY + 140.0f, 60, 60, 30.0f, 0xFF55DD55u); + NativeUI::DrawShadowText(640.0f, kPY + 170.0f, L"\u2713", + 0xFFFFFFFFu, 32.0f, + NativeUI::ALIGN_CENTER_X | NativeUI::ALIGN_CENTER_Y); + + NativeUI::DrawShadowText(640.0f, kPY + 218.0f, L"Signed in successfully!", + 0xFF55DD55u, 22.0f, NativeUI::ALIGN_CENTER_X); + + newauth::JavaSession s = auth.GetJavaSession(); + if (!s.username.empty()) + { + const std::wstring wname(s.username.begin(), s.username.end()); + NativeUI::DrawShadowText(640.0f, kPY + 252.0f, wname.c_str(), + 0xFFFFFFFFu, 18.0f, NativeUI::ALIGN_CENTER_X); + } + + // auto-close progress bar + float progress = closeCountdown > 0 ? (float)closeCountdown / kAutoCloseTicks : 1.0f; + NativeUI::DrawProgressBar(kPX + 100.0f, kPY + 290.0f, kPW - 200.0f, 6.0f, + progress, 0xFF55DD55u, 0xFF222222u); + NativeUI::DrawText(640.0f, kPY + 302.0f, L"Returning to account list...", + 0xFF888888u, 11.0f, NativeUI::ALIGN_CENTER_X); + } + // shit failed + else if (state == newauthManager::State::Failed) + { + // fat red x + NativeUI::DrawRoundedRect(640.0f - 30, kPY + 140.0f, 60, 60, 30.0f, 0xFFDD5555u); + NativeUI::DrawShadowText(640.0f, kPY + 170.0f, L"X", + 0xFFFFFFFFu, 28.0f, + NativeUI::ALIGN_CENTER_X | NativeUI::ALIGN_CENTER_Y); + + NativeUI::DrawShadowText(640.0f, kPY + 218.0f, L"Sign-in failed", + 0xFFDD5555u, 22.0f, NativeUI::ALIGN_CENTER_X); + + std::string err = auth.GetLastError(); + if (!err.empty()) + { + const std::wstring werr(err.begin(), err.end()); + NativeUI::DrawTextWrapped(640.0f, kPY + 252.0f, werr.c_str(), + kPW - 120.0f, 0xFF888888u, 12.0f, + NativeUI::ALIGN_CENTER_X); + } + } + // waiting for the user to enter the code + else if (state == newauthManager::State::WaitingForCode || + state == newauthManager::State::Authenticating) + { + const std::string code = auth.GetJavaDeviceCode(); + const std::string uri = auth.GetJavaDirectUri(); + + if (code.empty()) + { + // still loading... + NativeUI::DrawSpinner(640.0f, kPY + 200.0f, 24.0f, spinnerTick); + NativeUI::DrawShadowText(640.0f, kPY + 240.0f, L"Connecting to Microsoft...", + 0xFFAAAAAA, 14.0f, NativeUI::ALIGN_CENTER_X); + } + else + { + // step 1: go to the url + float stepY1 = kPY + 66.0f; + DrawStepCircle(kPX + 50.0f, stepY1 + 10.0f, 1, + state == newauthManager::State::Authenticating); + + NativeUI::DrawShadowText(kPX + 76.0f, stepY1 - 2.0f, + L"Open this link on any device:", + 0xFFCCCCCCu, 13.0f, NativeUI::ALIGN_LEFT); + + if (!uri.empty()) + { + const std::wstring wuri(uri.begin(), uri.end()); + NativeUI::DrawLink(640.0f, kPY + 128.0f, wuri.c_str(), + uri.c_str(), + focus.ShowFocus(eLink_URL), + focus.IsHovered(eLink_URL), + 15.0f, NativeUI::ALIGN_CENTER_X); + } + + // step 2: type the code in + float stepY2 = kPY + 168.0f; + DrawStepCircle(kPX + 50.0f, stepY2 + 10.0f, 2, false); + + NativeUI::DrawShadowText(kPX + 76.0f, stepY2 - 2.0f, + L"Enter this code:", + 0xFFCCCCCCu, 13.0f, NativeUI::ALIGN_LEFT); + + // code display box, make it big and obvious + const float bx = kPX + 120.0f, by2 = kPY + 200.0f; + const float bw = kPW - 240.0f, bh = 68.0f; + NativeUI::DrawTextBox(bx, by2, bw, bh); + + const std::wstring wcode(code.begin(), code.end()); + NativeUI::DrawShadowText(640.0f, by2 + 14.0f, wcode.c_str(), + 0xFFFFFFFFu, 32.0f, NativeUI::ALIGN_CENTER_X); + + // status text + float statusY = kPY + 290.0f; + NativeUI::DrawSpinner(600.0f, statusY + 6.0f, 8.0f, spinnerTick); + const wchar_t* status = (state == newauthManager::State::Authenticating) + ? L"Authenticating..." + : L"Waiting for you to sign in..."; + NativeUI::DrawText(616.0f, statusY, status, + 0xFF888888u, 12.0f, NativeUI::ALIGN_LEFT); + } + } + else + { + // idle, waiting + NativeUI::DrawSpinner(640.0f, kPY + 220.0f, 20.0f, spinnerTick); + } + + // divider + NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); + + // cancel button + NativeUI::DrawButton(kPX + (kPW - 200.0f) * 0.5f, kBtnY, 200.0f, kBtnH, + L"Cancel", + focus.ShowFocus(eBtn_Back), focus.IsHovered(eBtn_Back)); +} + + +// offline username input + +static void RenderOfflineInput(const NativeUI::NineSlice& panel, + const std::string& username, + int cursorBlink, + NativeUI::FocusList& focus, + bool textInputActive) +{ + if (panel.valid) + NativeUI::DrawNineSlice(kPX, kPY, kPW, kPH, panel); + else + NativeUI::DrawPanel(kPX, kPY, kPW, kPH); + + NativeUI::DrawShadowText(640.0f, kPY + 14.0f, L"Add Offline Account", + 0xFFFFFFFFu, 22.0f, NativeUI::ALIGN_CENTER_X); + + NativeUI::DrawDivider(kPX + 20.0f, kPY + 44.0f, kPW - 40.0f, 0x40FFFFFFu); + + // desc text + NativeUI::DrawShadowText(640.0f, kPY + 68.0f, + L"Choose a username for offline play", + 0xFFDDDDDDu, 16.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawShadowText(640.0f, kPY + 92.0f, + L"Letters, numbers and underscores only (max 16)", + 0xFFAAAAAAu, 13.0f, NativeUI::ALIGN_CENTER_X); + + // label + NativeUI::DrawShadowText(640.0f, kPY + 134.0f, L"Username", + 0xFFDDDDDDu, 14.0f, NativeUI::ALIGN_CENTER_X); + + // text input box + const float bx = kPX + 120.0f, by = kPY + 156.0f; + const float bw = kPW - 240.0f, bh = 52.0f; + + bool tbFocused = focus.ShowFocus(eBtn_OfflineTextBox); + bool tbHovered = focus.IsHovered(eBtn_OfflineTextBox); + + // text box with focus border + NativeUI::DrawTextBox(bx, by, bw, bh, + (tbFocused || textInputActive) ? 0xFFCCCCFFu : 0xFFFFFFFFu); + + if (tbFocused || tbHovered) + NativeUI::DrawRoundedBorder(bx - 1, by - 1, bw + 2, bh + 2, 2.0f, 1.5f, + textInputActive ? 0xFFFFFF55u : 0xAAFFFFFFu); + + // show what theyve typed + std::wstring display(username.begin(), username.end()); + + // blink cursor when focused + if (textInputActive && (cursorBlink / 25) % 2 == 0) + display += L"_"; + + if (display.empty()) + { + // Placeholder text + const wchar_t* hint = textInputActive + ? L"Type a username..." + : L"Select to type a username"; + NativeUI::DrawText(bx + 14.0f, by + 14.0f, hint, + 0xFF555555u, 18.0f, NativeUI::ALIGN_LEFT); + } + + NativeUI::DrawShadowText(bx + 14.0f, by + 14.0f, display.c_str(), + 0xFFFFFFFFu, 20.0f, NativeUI::ALIGN_LEFT); + + // char count + { + char hint[32]; + snprintf(hint, sizeof(hint), "%d / 16", (int)username.size()); + std::wstring whint(hint, hint + strlen(hint)); + uint32_t hintColor = username.size() >= 14 ? 0xFFDD8855u : 0xFF666666u; + NativeUI::DrawText(bx + bw - 8.0f, by + bh + 6.0f, whint.c_str(), + hintColor, 10.0f, NativeUI::ALIGN_RIGHT); + } + + // steve head preview + float previewY = kPY + 240.0f; + if (!username.empty()) + { + DrawHeadPlaceholder(640.0f - 24.0f, previewY, 48.0f); + std::wstring wname(username.begin(), username.end()); + NativeUI::DrawShadowText(640.0f, previewY + 56.0f, wname.c_str(), + 0xFFFFFFFFu, 14.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawText(640.0f, previewY + 74.0f, L"Offline Account", + 0xFFAAAA55u, 10.0f, NativeUI::ALIGN_CENTER_X); + } + + // divider + NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); + + // buttons + float totalBtnW = 200.0f + 12.0f + 200.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + NativeUI::DrawButton(btnStartX, kBtnY, 200.0f, kBtnH, L"Confirm", + focus.ShowFocus(eBtn_OfflineConfirm), + focus.IsHovered(eBtn_OfflineConfirm)); + NativeUI::DrawButton(btnStartX + 212.0f, kBtnY, 200.0f, kBtnH, L"Cancel", + focus.ShowFocus(eBtn_Back), focus.IsHovered(eBtn_Back)); +} + + +// ely.by login form + +static void RenderElybyInput(const NativeUI::NineSlice& panel, + const std::string& username, + const std::string& password, + int cursorBlink, + NativeUI::FocusList& focus, + bool textInputActive, + int activeField) +{ + if (panel.valid) + NativeUI::DrawNineSlice(kPX, kPY, kPW, kPH, panel); + else + NativeUI::DrawPanel(kPX, kPY, kPW, kPH); + + NativeUI::DrawShadowText(640.0f, kPY + 14.0f, L"Sign In with Ely.by", + 0xFFFFFFFFu, 22.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawDivider(kPX + 20.0f, kPY + 44.0f, kPW - 40.0f, 0x40FFFFFFu); + + NativeUI::DrawShadowText(640.0f, kPY + 62.0f, + L"Enter your ely.by username and password", + 0xFFDDDDDDu, 14.0f, NativeUI::ALIGN_CENTER_X); + + const float bx = kPX + 120.0f, bw = kPW - 240.0f, bh = 44.0f; + + // username + NativeUI::DrawShadowText(bx, kPY + 112.0f, L"Username / Email", + 0xFFDDDDDDu, 12.0f, NativeUI::ALIGN_LEFT); + { + bool tbFocused = focus.ShowFocus(eBtn_ElybyUsername); + bool tbHovered = focus.IsHovered(eBtn_ElybyUsername); + bool isActive = textInputActive && activeField == 0; + NativeUI::DrawTextBox(bx, kPY + 130.0f, bw, bh, + isActive ? 0xFFCCCCFFu : 0xFFFFFFFFu); + if (tbFocused || tbHovered) + NativeUI::DrawRoundedBorder(bx - 1, kPY + 129.0f, bw + 2, bh + 2, 2.0f, 1.5f, + isActive ? 0xFFFFFF55u : 0xAAFFFFFFu); + std::wstring display(username.begin(), username.end()); + if (isActive && (cursorBlink / 25) % 2 == 0) display += L"_"; + if (display.empty()) + NativeUI::DrawText(bx + 10.0f, kPY + 142.0f, L"Username or email...", + 0xFF555555u, 16.0f, NativeUI::ALIGN_LEFT); + NativeUI::DrawShadowText(bx + 10.0f, kPY + 142.0f, display.c_str(), + 0xFFFFFFFFu, 16.0f, NativeUI::ALIGN_LEFT); + } + + // password + NativeUI::DrawShadowText(bx, kPY + 192.0f, L"Password", + 0xFFDDDDDDu, 12.0f, NativeUI::ALIGN_LEFT); + { + bool tbFocused = focus.ShowFocus(eBtn_ElybyPassword); + bool tbHovered = focus.IsHovered(eBtn_ElybyPassword); + bool isActive = textInputActive && activeField == 1; + NativeUI::DrawTextBox(bx, kPY + 210.0f, bw, bh, + isActive ? 0xFFCCCCFFu : 0xFFFFFFFFu); + if (tbFocused || tbHovered) + NativeUI::DrawRoundedBorder(bx - 1, kPY + 209.0f, bw + 2, bh + 2, 2.0f, 1.5f, + isActive ? 0xFFFFFF55u : 0xAAFFFFFFu); + // Render dots for password + std::wstring dots(password.size(), L'\u2022'); + if (isActive && (cursorBlink / 25) % 2 == 0) dots += L"_"; + if (dots.empty()) + NativeUI::DrawText(bx + 10.0f, kPY + 222.0f, L"Password...", + 0xFF555555u, 16.0f, NativeUI::ALIGN_LEFT); + NativeUI::DrawShadowText(bx + 10.0f, kPY + 222.0f, dots.c_str(), + 0xFFFFFFFFu, 16.0f, NativeUI::ALIGN_LEFT); + } + + // divider + NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); + + // buttons + float totalBtnW = 160.0f + 12.0f + 160.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + NativeUI::DrawButton(btnStartX, kBtnY, 160.0f, kBtnH, L"Sign In", + focus.ShowFocus(eBtn_ElybySignIn), focus.IsHovered(eBtn_ElybySignIn)); + NativeUI::DrawButton(btnStartX + 172.0f, kBtnY, 160.0f, kBtnH, L"Cancel", + focus.ShowFocus(eBtn_ElybyCancel), focus.IsHovered(eBtn_ElybyCancel)); +} + + +// ely.by 2fa input + +static void RenderElyby2FA(const NativeUI::NineSlice& panel, + const std::string& code, + int cursorBlink, + NativeUI::FocusList& focus, + bool textInputActive) +{ + if (panel.valid) + NativeUI::DrawNineSlice(kPX, kPY, kPW, kPH, panel); + else + NativeUI::DrawPanel(kPX, kPY, kPW, kPH); + + NativeUI::DrawShadowText(640.0f, kPY + 14.0f, L"Two-Factor Authentication", + 0xFFFFFFFFu, 22.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawDivider(kPX + 20.0f, kPY + 44.0f, kPW - 40.0f, 0x40FFFFFFu); + + NativeUI::DrawShadowText(640.0f, kPY + 80.0f, + L"Your account is protected with 2FA.", + 0xFFDDDDDDu, 14.0f, NativeUI::ALIGN_CENTER_X); + NativeUI::DrawShadowText(640.0f, kPY + 100.0f, + L"Enter the code from your authenticator app.", + 0xFFAAAAAAu, 13.0f, NativeUI::ALIGN_CENTER_X); + + // totp code box + NativeUI::DrawShadowText(640.0f, kPY + 140.0f, L"Authenticator Code", + 0xFFDDDDDDu, 12.0f, NativeUI::ALIGN_CENTER_X); + + const float bx = kPX + 160.0f, bw = kPW - 320.0f, bh = 44.0f; + { + bool tbFocused = focus.ShowFocus(eBtn_Elyby2FACode); + bool tbHovered = focus.IsHovered(eBtn_Elyby2FACode); + NativeUI::DrawTextBox(bx, kPY + 160.0f, bw, bh, + textInputActive ? 0xFFCCCCFFu : 0xFFFFFFFFu); + if (tbFocused || tbHovered) + NativeUI::DrawRoundedBorder(bx - 1, kPY + 159.0f, bw + 2, bh + 2, 2.0f, 1.5f, + textInputActive ? 0xFFFFFF55u : 0xAAFFFFFFu); + std::wstring display(code.begin(), code.end()); + if (textInputActive && (cursorBlink / 25) % 2 == 0) display += L"_"; + if (display.empty()) + NativeUI::DrawText(bx + 10.0f, kPY + 172.0f, L"Enter code...", + 0xFF555555u, 18.0f, NativeUI::ALIGN_LEFT); + NativeUI::DrawShadowText(640.0f, kPY + 172.0f, display.c_str(), + 0xFFFFFFFFu, 20.0f, NativeUI::ALIGN_CENTER_X); + } + + // divider + NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); + + float totalBtnW = 160.0f + 12.0f + 160.0f; + float btnStartX = kPX + (kPW - totalBtnW) * 0.5f; + NativeUI::DrawButton(btnStartX, kBtnY, 160.0f, kBtnH, L"Submit", + focus.ShowFocus(eBtn_Elyby2FASubmit), focus.IsHovered(eBtn_Elyby2FASubmit)); + NativeUI::DrawButton(btnStartX + 172.0f, kBtnY, 160.0f, kBtnH, L"Cancel", + focus.ShowFocus(eBtn_Elyby2FACancel), focus.IsHovered(eBtn_Elyby2FACancel)); +} + + +void UIScene_MSAuth::render(S32 /*width*/, S32 /*height*/, + C4JRender::eViewportType /*viewport*/) +{ + if (!m_hasTickedOnce) return; + + if (!m_panelLoaded) + { + m_panel = NativeUI::LoadNineSlice( + "Common/Media/Graphics/PanelsAndTabs/Panel"); + m_recessPanel = NativeUI::LoadNineSlice( + "Common/Media/Graphics/PanelsAndTabs/Panel_Recess"); + m_panelLoaded = true; + } + + NativeUI::BeginFrame(); + + // dim everything + NativeUI::DrawRectFullscreen(0xB0000000u); + + if (m_view == eView_AccountList) + { + RenderAccountList(m_panel, m_recessPanel, m_accounts, m_activeIdx, + m_scrollOffset, m_spinnerTick, m_focus, m_skinCache, + m_pendingRemoveIdx, m_targetSlot); + } + else if (m_view == eView_OfflineInput) + { + RenderOfflineInput(m_panel, m_offlineUsername, m_offlineCursorBlink, m_focus, + m_textInputActive); + } + else if (m_view == eView_ElybyInput) + { + RenderElybyInput(m_panel, m_elybyUsername, m_elybyPassword, + m_offlineCursorBlink, m_focus, m_textInputActive, + m_elybyActiveField); + } + else if (m_view == eView_Elyby2FA) + { + RenderElyby2FA(m_panel, m_elyby2FACode, m_offlineCursorBlink, m_focus, + m_textInputActive); + } + else + { + RenderDeviceCode(m_panel, m_spinnerTick, m_closeCountdown, + m_authFlags->success.load(std::memory_order_relaxed), + m_focus, newauthManager::Get()); + } + + NativeUI::EndFrame(); +} + + +// input handling + +void UIScene_MSAuth::updateTooltips() +{ + ui.SetTooltips(m_iPad, IDS_TOOLTIPS_SELECT, IDS_TOOLTIPS_BACK); +} + +void UIScene_MSAuth::handleInput(int iPad, int key, bool repeat, + bool pressed, bool released, bool& handled) +{ + handled = true; + + if (!pressed) return; + + if (m_inputGuardTicks > 0) return; + + if (m_view == eView_OfflineInput && key == ACTION_MENU_CANCEL) + { + ui.PlayUISFX(eSFX_Back); + if (m_textInputActive) + m_textInputActive = false; + else if (!m_offlineUsername.empty()) + m_offlineUsername.clear(); + else + handlePress((F64)eBtn_Back, 0.0); + return; + } + + if ((m_view == eView_ElybyInput || m_view == eView_Elyby2FA) && key == ACTION_MENU_CANCEL) + { + ui.PlayUISFX(eSFX_Back); + if (m_textInputActive) + m_textInputActive = false; + else + SwitchToAccountList(); + return; + } + + // scroll the account list when at the edge instead of jumping to buttons + if (m_view == eView_AccountList && m_pendingRemoveIdx < 0 + && (int)m_accounts.size() > kMaxVisible) + { + int foc = m_focus.GetFocused(); + // Get the account index if focused on an account row or its remove button + int focAcctIdx = -1; + if (foc >= eAccountBase) + focAcctIdx = foc - eAccountBase; + else if (foc >= eBtn_RemoveBase && foc < eAccountBase) + focAcctIdx = foc - eBtn_RemoveBase; + + if (focAcctIdx >= 0) + { + int lastVisible = m_scrollOffset + kMaxVisible - 1; + + if ((key == ACTION_MENU_DOWN || key == ACTION_MENU_RIGHT) + && focAcctIdx >= lastVisible + && m_scrollOffset + kMaxVisible < (int)m_accounts.size()) + { + ++m_scrollOffset; + ui.PlayUISFX(eSFX_Focus); + return; + } + if ((key == ACTION_MENU_UP || key == ACTION_MENU_LEFT) + && focAcctIdx <= m_scrollOffset + && m_scrollOffset > 0) + { + --m_scrollOffset; + ui.PlayUISFX(eSFX_Focus); + return; + } + } + } + +#ifdef _WINDOWS64 + // mouse wheel scrolling + if (m_view == eView_AccountList && m_pendingRemoveIdx < 0) + { + int wheel = g_KBMInput.GetMouseWheel(); + if (wheel != 0) + { + int maxScroll = (int)m_accounts.size() - kMaxVisible; + if (maxScroll > 0) + { + m_scrollOffset -= wheel; + if (m_scrollOffset < 0) m_scrollOffset = 0; + if (m_scrollOffset > maxScroll) m_scrollOffset = maxScroll; + } + } + } +#endif + + int backId = (m_pendingRemoveIdx >= 0) ? eBtn_ConfirmNo : eBtn_Back; + int result = m_focus.HandleMenuKey(key, backId, kPX, kPY, kPW, kPH); + + if (result == NativeUI::FocusList::RESULT_UNHANDLED) + { + return; + } + + if (result == NativeUI::FocusList::RESULT_NAVIGATED) + { + return; + } + + handlePress((F64)result, 0.0); +} + +void UIScene_MSAuth::handlePress(F64 controlId, F64 /*childId*/) +{ + int id = static_cast(controlId); + + // remove confirm dialog + if (m_pendingRemoveIdx >= 0) + { + if (id == eBtn_ConfirmYes) + { + int idx = m_pendingRemoveIdx; + m_pendingRemoveIdx = -1; + + // Verify the index still refers to the same account (stale-index safety) + auto accounts = newauthManager::Get().GetJavaAccounts(); + if (idx < 0 || idx >= (int)accounts.size() || accounts[idx].uuid != m_pendingRemoveUuid) { + m_pendingRemoveUuid.clear(); + return; // list changed — abort removal silently + } + m_pendingRemoveUuid.clear(); + + newauthManager::Get().RemoveJavaAccount(idx); + newauthManager::Get().SaveJavaAccountIndex(); + + accounts = newauthManager::Get().GetJavaAccounts(); + int newActive = newauthManager::Get().GetActiveJavaAccountIndex(); + if (!accounts.empty() && newActive >= 0) + newauthManager::Get().SetAccountForSlot(m_targetSlot, newActive); + } + else // ConfirmNo, Back, or any other key → cancel + { + m_pendingRemoveIdx = -1; + } + return; + } + + if (id == eBtn_Back) + { + if (m_view == eView_DeviceCode || m_view == eView_OfflineInput || + m_view == eView_ElybyInput || m_view == eView_Elyby2FA) + { + SwitchToAccountList(); + } + else + { +#ifdef _WINDOWS64 + if (m_targetSlot > 0) + { + // TODO: splitscreen auth callback not yet implemented + // Minecraft* mc = Minecraft::GetInstance(); + // if (mc) mc->setSplitAuthCancelled(m_targetSlot); + } +#endif + ui.SetTooltips(m_iPad, -1, -1); + ui.NavigateBack(m_iPad); + } + return; + } + + if (m_view == eView_ElybyInput) + { + if (id == eBtn_ElybyUsername || id == eBtn_ElybyPassword) + { +#ifdef _WINDOWS64 + bool useGamepadKeyboard = + (m_focus.GetLastDevice() == NativeUI::FocusList::eDevice_Gamepad); + if (!useGamepadKeyboard) + { + m_elybyActiveField = (id == eBtn_ElybyUsername) ? 0 : 1; + m_textInputActive = true; + g_KBMInput.ClearCharBuffer(); + } + else + { + m_elybyActiveField = (id == eBtn_ElybyUsername) ? 0 : 1; + m_pendingKBResult = std::make_shared(); + UIKeyboardInitData kbData; + kbData.title = (id == eBtn_ElybyUsername) ? L"Ely.by Username" : L"Password"; + kbData.defaultText = L""; + kbData.maxChars = 128; + kbData.callback = &MSAuthKeyboardCallback; + kbData.lpParam = new std::shared_ptr(m_pendingKBResult); + ui.NavigateToScene(m_iPad, eUIScene_Keyboard, &kbData, + eUILayer_Fullscreen, eUIGroup_Fullscreen); + } +#endif + } + else if (id == eBtn_ElybySignIn) + { + m_textInputActive = false; + SubmitElybyLogin(); + } + else if (id == eBtn_ElybyCancel) + { + m_textInputActive = false; + SwitchToAccountList(); + } + return; + } + + if (m_view == eView_Elyby2FA) + { + if (id == eBtn_Elyby2FACode) + { +#ifdef _WINDOWS64 + bool useGamepadKeyboard = + (m_focus.GetLastDevice() == NativeUI::FocusList::eDevice_Gamepad); + if (!useGamepadKeyboard) + { + m_textInputActive = true; + g_KBMInput.ClearCharBuffer(); + } + else + { + m_pendingKBResult = std::make_shared(); + UIKeyboardInitData kbData; + kbData.title = L"2FA Code"; + kbData.defaultText = L""; + kbData.maxChars = 10; + kbData.callback = &MSAuthKeyboardCallback; + kbData.lpParam = new std::shared_ptr(m_pendingKBResult); + ui.NavigateToScene(m_iPad, eUIScene_Keyboard, &kbData, + eUILayer_Fullscreen, eUIGroup_Fullscreen); + } +#endif + } + else if (id == eBtn_Elyby2FASubmit) + { + m_textInputActive = false; + SubmitElyby2FA(); + } + else if (id == eBtn_Elyby2FACancel) + { + m_textInputActive = false; + SwitchToAccountList(); + } + return; + } + + if (m_view == eView_OfflineInput) + { + if (id == eBtn_OfflineTextBox) + { +#ifdef _WINDOWS64 + bool useGamepadKeyboard = + (m_focus.GetLastDevice() == NativeUI::FocusList::eDevice_Gamepad); + + if (!useGamepadKeyboard) + { + // KBM (keyboard/mouse): activate inline text input + m_textInputActive = true; + g_KBMInput.ClearCharBuffer(); + } + else + { + // Gamepad: open the game's virtual keyboard scene + m_pendingKBResult = std::make_shared(); + UIKeyboardInitData kbData; + kbData.title = L"Offline Username"; + kbData.defaultText = m_offlineUsername.empty() ? L"" + : std::wstring(m_offlineUsername.begin(), m_offlineUsername.end()).c_str(); + kbData.maxChars = 16; + kbData.callback = &MSAuthKeyboardCallback; + kbData.lpParam = new std::shared_ptr(m_pendingKBResult); + ui.NavigateToScene(m_iPad, eUIScene_Keyboard, &kbData, + eUILayer_Fullscreen, eUIGroup_Fullscreen); + } +#endif + } + else if (id == eBtn_OfflineConfirm) + { + m_textInputActive = false; + ConfirmOfflineAccount(); + } + return; + } + + if (m_view == eView_AccountList) + { + if (id == eBtn_AddAccount) + { + SwitchToDeviceCode(); + } + else if (id == eBtn_AddElyby) + { + SwitchToElybyInput(); + } + else if (id == eBtn_AddOffline) + { + SwitchToOfflineInput(); + } + else if (id >= eBtn_RemoveBase && id < eAccountBase) + { + // Show confirmation dialog instead of removing immediately. + // Capture the UUID so we can verify at confirm time that the + // index still refers to the same account (stale-index safety). + int removeIdx = id - eBtn_RemoveBase; + if (removeIdx >= 0 && removeIdx < (int)m_accounts.size()) { + m_pendingRemoveIdx = removeIdx; + m_pendingRemoveUuid = m_accounts[removeIdx].uuid; + } + } + else if (id >= eAccountBase) + { + int idx = id - eAccountBase; + if (newauthManager::Get().IsAccountInUseByOtherSlot(m_targetSlot, idx)) + { + app.DebugPrintf("[MSAuth] Account %d already in use by another player slot\n", idx); + } + else + { + newauthManager::Get().SetAccountForSlot(m_targetSlot, idx); + +#ifdef _WINDOWS64 + if (m_targetSlot > 0) + { + // TODO: splitscreen auth callback not yet implemented + // Minecraft* mc = Minecraft::GetInstance(); + // if (mc) mc->setSplitAuthCompleted(m_targetSlot); + ui.SetTooltips(m_iPad, -1, -1); + ui.NavigateBack(m_iPad); + } +#endif + } + } + } + else if (m_view == eView_DeviceCode) + { + if (id == eLink_URL && !m_cachedUri.empty()) + NativeUI::OpenURL(m_cachedUri.c_str()); + } +} + + +// mouse click (win64) + +#ifdef _WINDOWS64 +bool UIScene_MSAuth::handleMouseClick(F32 /*x*/, F32 /*y*/) +{ + return m_focus.IsMouseConsumed(); +} +#endif diff --git a/Minecraft.Client/Common/UI/UIScene_MSAuth.h b/Minecraft.Client/Common/UI/UIScene_MSAuth.h new file mode 100644 index 0000000000..9e693ada06 --- /dev/null +++ b/Minecraft.Client/Common/UI/UIScene_MSAuth.h @@ -0,0 +1,154 @@ +#pragma once +/* + * account manager ui + * two views: account list and device code flow + * no swf, pure native overlay + */ + +#include "UIScene.h" +#include "NativeUIRenderer.h" +#include "../../../newauth/include/newauthManager.h" +#include +#include +#include +#include + +// control ids for the focus list +// file scope so the static render helpers can use them +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 + idx = remove btn + eAccountBase = 100, // 100 + idx = account row + eLink_URL = 200, + }; +} + +class UIScene_MSAuth : public UIScene +{ +private: + // what were looking at + enum EView { eView_AccountList, eView_DeviceCode, eView_OfflineInput, eView_ElybyInput, eView_Elyby2FA }; + EView m_view = eView_AccountList; + + // shared flags that survive scene destruction (async callback safety) + struct AuthFlags { + std::atomic done { false }; + std::atomic success{ false }; + std::atomic need2FA{ false }; + }; + std::shared_ptr m_authFlags = std::make_shared(); + + int m_closeCountdown = 0; + int m_spinnerTick = 0; + ULONGLONG m_lastAnimTime = 0; // throttle animations to 30 FPS + NativeUI::NineSlice m_panel; + NativeUI::NineSlice m_recessPanel; + bool m_panelLoaded = false; + NativeUI::FocusList m_focus; + std::string m_cachedUri; + + // accounts snapshot, refreshed every tick + std::vector m_accounts; + int m_activeIdx = -1; + + // scroll pos for the list + int m_scrollOffset = 0; + + void StartAddAccount(); + void SwitchToAccountList(); + void SwitchToDeviceCode(); + void SwitchToOfflineInput(); + void SwitchToElybyInput(); + void SwitchToElyby2FA(); + void ConfirmOfflineAccount(); + void SubmitElybyLogin(); + void SubmitElyby2FA(); + + // offline name entry + std::string m_offlineUsername; + int m_offlineCursorBlink = 0; + bool m_textInputActive = false; // typing mode on/off + + // ely.by creds + std::string m_elybyUsername; + std::string m_elybyPassword; + std::string m_elyby2FACode; + // (2fa flag in m_authFlags->need2FA cuz async) + int m_elybyActiveField = 0; // 0=user, 1=pass, 2=2fa + + // which splitscreen slot this is for (0 = main, 1-3 = controllers) + int m_targetSlot = 0; + + // eat input for a few ticks when opening so we dont accidentally + // process the button press that opened this damn scene + int m_inputGuardTicks = 6; + + // remove confirm dialog (-1 = hidden, >= 0 = which account to nuke) + int m_pendingRemoveIdx = -1; + std::string m_pendingRemoveUuid; // saved uuid in case the index changes under us + +public: + // shared buffer for virtual keyboard (public because the static + // callback func needs it, prevents use-after-free if scene dies + // while keyboard is up on another ui group) + struct PendingKeyboardResult { + std::string value; + std::atomic ready{false}; + std::atomic valid{true}; + }; + + // skin cache entry (public for static render helpers) + struct SkinEntry { + std::atomic textureId{-2}; // -2=nope, -1=loading/failed, -3=file ready, >=0=loaded + std::string filePath; + }; + +private: + std::shared_ptr m_pendingKBResult; + + // skin texture cache keyed by uuid + std::unordered_map> 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 +}; diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index 93f1edf119..8668cb10f2 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -33,6 +33,9 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye m_bIgnorePress=false; +#ifdef _WINDOWS64 + m_bPendingPlayAfterAuth = false; +#endif m_buttons[static_cast(eControl_PlayGame)].init(IDS_PLAY_GAME,eControl_PlayGame); #ifdef _XBOX_ONE @@ -43,6 +46,10 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye m_buttons[static_cast(eControl_Leaderboards)].init(IDS_LEADERBOARDS,eControl_Leaderboards); m_buttons[static_cast(eControl_Achievements)].init( (UIString)IDS_ACHIEVEMENTS,eControl_Achievements); m_buttons[static_cast(eControl_HelpAndOptions)].init(IDS_HELP_AND_OPTIONS,eControl_HelpAndOptions); +#ifdef _WINDOWS64 + m_bTrialVersion=false; + m_buttons[static_cast(eControl_UnlockOrDLC)].init(L"Auth / Accounts",eControl_UnlockOrDLC); +#else if(ProfileManager.IsFullVersion()) { m_bTrialVersion=false; @@ -53,6 +60,7 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye m_bTrialVersion=true; m_buttons[static_cast(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC); } +#endif #ifndef _DURANGO m_buttons[static_cast(eControl_Exit)].init(app.GetString(IDS_EXIT_GAME),eControl_Exit); @@ -153,6 +161,15 @@ void UIScene_MainMenu::handleGainFocus(bool navBack) ui.ShowPlayerDisplayname(false); m_bIgnorePress=false; +#ifdef _WINDOWS64 + if (navBack && m_bPendingPlayAfterAuth) + { + m_bPendingPlayAfterAuth = false; + proceedToScene(ProfileManager.GetPrimaryPad(), eUIScene_LoadOrJoinMenu); + return; + } +#endif + if (eNavigateWhenReady >= 0) { return; @@ -181,8 +198,10 @@ void UIScene_MainMenu::handleGainFocus(bool navBack) if(navBack && ProfileManager.IsFullVersion()) { +#ifndef _WINDOWS64 // Replace the Unlock Full Game with Downloadable Content m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT); +#endif } #if TO_BE_IMPLEMENTED @@ -356,8 +375,12 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId) //CD - Added for audio ui.PlayUISFX(eSFX_Press); +#ifdef _WINDOWS64 + ui.NavigateToScene(primaryPad, eUIScene_MSAuth); +#else m_eAction=eAction_RunUnlockOrDLC; signInReturnedFunc = &UIScene_MainMenu::UnlockFullGame_SignInReturned; +#endif break; case eControl_Exit: //CD - Added for audio @@ -1419,7 +1442,22 @@ void UIScene_MainMenu::RunPlayGame(int iPad) #ifdef _XBOX_ONE ui.ShowPlayerDisplayname(true); #endif +#ifdef _WINDOWS64 + { + const auto& slot = newauthManager::Get().GetSlot(0); + if (slot.accountIndex < 0 && newauthManager::Get().GetJavaAccounts().empty()) + { + m_bPendingPlayAfterAuth = true; + ui.NavigateToScene(ProfileManager.GetPrimaryPad(), eUIScene_MSAuth); + } + else + { + proceedToScene(ProfileManager.GetPrimaryPad(), eUIScene_LoadOrJoinMenu); + } + } +#else proceedToScene(ProfileManager.GetPrimaryPad(), eUIScene_LoadOrJoinMenu); +#endif #endif } else @@ -2129,7 +2167,9 @@ void UIScene_MainMenu::LoadTrial(void) void UIScene_MainMenu::handleUnlockFullVersion() { +#ifndef _WINDOWS64 m_buttons[static_cast(eControl_UnlockOrDLC)].setLabel(IDS_DOWNLOADABLECONTENT,true); +#endif } diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.h b/Minecraft.Client/Common/UI/UIScene_MainMenu.h index 2b49a44b3d..ab93a6098b 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.h @@ -1,6 +1,9 @@ #pragma once #include "UIScene.h" +#ifdef _WINDOWS64 +#include "../../../newauth/include/newauthManager.h" +#endif class UIScene_MainMenu : public UIScene { @@ -56,6 +59,9 @@ class UIScene_MainMenu : public UIScene #ifdef _XBOX_ONE bool m_bWaitingForDLCInfo; #endif +#ifdef _WINDOWS64 + bool m_bPendingPlayAfterAuth; +#endif float m_fScreenWidth,m_fScreenHeight; float m_fRawWidth,m_fRawHeight; diff --git a/Minecraft.Client/Extrax64Stubs.cpp b/Minecraft.Client/Extrax64Stubs.cpp index 0147896ca9..ac58fa0bf8 100644 --- a/Minecraft.Client/Extrax64Stubs.cpp +++ b/Minecraft.Client/Extrax64Stubs.cpp @@ -238,9 +238,32 @@ ULONG_PTR IQNetPlayer::GetCustomDataValue() { IQNetPlayer IQNet::m_player[MINECRAFT_NET_MAX_PLAYERS]; DWORD IQNet::s_playerCount = 1; bool IQNet::s_isHosting = true; +static bool s_joinedDedicatedServer = false; +// tldr checking if its a dedicated server or a singleplayer QNET_STATE _iQNetStubState = QNET_STATE_IDLE; +void Win64_SetJoinedDedicatedServer(bool isDedicated) +{ + s_joinedDedicatedServer = isDedicated; +} + +bool Win64_IsJoinedDedicatedServer() +{ + return s_joinedDedicatedServer; +} + +bool Win64_ShouldExposeSessionPlayer(IQNetPlayer *player) +{ + if (player == nullptr) + return false; + + if (!IQNet::s_isHosting && s_joinedDedicatedServer && player->m_isRemote && player->m_isHostPlayer) + return false; + + return true; +} + void Win64_SetupRemoteQNetPlayer(IQNetPlayer * player, BYTE smallId, bool isHost, bool isLocal) { player->m_smallId = smallId; @@ -317,7 +340,7 @@ IQNetPlayer* IQNet::GetPlayerByIndex(DWORD dwPlayerIndex) DWORD found = 0; for (DWORD i = 0; i < s_playerCount; i++) { - if (Win64_IsActivePlayer(&m_player[i], i)) + if (Win64_IsActivePlayer(&m_player[i], i) && Win64_ShouldExposeSessionPlayer(&m_player[i])) { if (found == dwPlayerIndex) return &m_player[i]; found++; @@ -351,7 +374,7 @@ DWORD IQNet::GetPlayerCount() DWORD count = 0; for (DWORD i = 0; i < s_playerCount; i++) { - if (Win64_IsActivePlayer(&m_player[i], i)) count++; + if (Win64_IsActivePlayer(&m_player[i], i) && Win64_ShouldExposeSessionPlayer(&m_player[i])) count++; } return count; } @@ -362,13 +385,15 @@ void IQNet::HostGame() { _iQNetStubState = QNET_STATE_SESSION_STARTING; s_isHosting = true; - // Host slot keeps legacy XUID so old host player data remains addressable. + s_joinedDedicatedServer = false; + // linking xuid to new uuid, lazy but it works. m_player[0].m_resolvedXuid = Win64Xuid::GetLegacyEmbeddedHostXuid(); } void IQNet::ClientJoinGame() { _iQNetStubState = QNET_STATE_SESSION_STARTING; s_isHosting = false; + s_joinedDedicatedServer = false; for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) { @@ -384,6 +409,7 @@ void IQNet::EndGame() { _iQNetStubState = QNET_STATE_IDLE; s_isHosting = false; + s_joinedDedicatedServer = false; s_playerCount = 1; for (int i = 0; i < MINECRAFT_NET_MAX_PLAYERS; i++) { diff --git a/Minecraft.Client/Font.h b/Minecraft.Client/Font.h index 58bceb4c09..138ff3b727 100644 --- a/Minecraft.Client/Font.h +++ b/Minecraft.Client/Font.h @@ -93,6 +93,15 @@ class Font void setEnforceUnicodeSheet(bool enforceUnicodeSheet); void setBidirectional(bool bidirectional); + int getCols() const { return m_cols; } + int getRows() const { return m_rows; } + int getCharWidth() const { return m_charWidth; } + int getCharHeight() const { return m_charHeight; } + ResourceLocation* getTextureLocation() const { return m_textureLocation; } + Textures* getTextures() const { return textures; } + wchar_t mapChar(wchar_t c) { return (wchar_t)MapCharacter(c); } + int getCharPixelWidth(wchar_t c) const { return charWidths[(int)(unsigned short)c]; } + // 4J-PB - check for invalid player name - Japanese local name bool AllCharactersValid(const wstring &str); }; diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 27ee68b65f..b7f6afd46e 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -33,6 +33,7 @@ #include "..\Minecraft.World\net.minecraft.world.damagesource.h" #ifdef _WINDOWS64 #include "Windows64\Network\WinsockNetLayer.h" +#include "..\newauth\include\newauthManager.h" #endif #include #ifdef SPLIT_SAVES @@ -644,10 +645,33 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW // 4J - Unused //localIp = settings->getString(L"server-ip", L""); - //onlineMode = settings->getBoolean(L"online-mode", true); //motd = settings->getString(L"motd", L"A Minecraft Server"); //motd.replace('�', '$'); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + { + wstring wProvider = settings->getString(L"auth-provider", L"mojang"); + authProvider = std::string(wProvider.begin(), wProvider.end()); + if (authProvider != "elyby" && authProvider != "offline") + authProvider = "mojang"; + } +#elif defined(_WINDOWS64) +// // singleplayer worlds should always be in offline mode right? +// // fuck ts makes me upset considering LAN worlds too +// yeah nvm that shit works now + { + newauthManager& authMgr = newauthManager::Get(); + auto accounts = authMgr.GetJavaAccounts(); + int activeIdx = authMgr.GetActiveJavaAccountIndex(); + if (activeIdx >= 0 && activeIdx < (int)accounts.size() && !accounts[activeIdx].authProvider.empty()) + authProvider = accounts[activeIdx].authProvider; + else + authProvider = "mojang"; + } +#else + authProvider = "mojang"; +#endif + setAnimals(GetDedicatedServerBool(settings, L"spawn-animals", true)); setNpcsEnabled(GetDedicatedServerBool(settings, L"spawn-npcs", true)); setPvpAllowed(app.GetGameHostOption( eGameHostOption_PvP )>0?true:false); @@ -680,11 +704,11 @@ bool MinecraftServer::initServer(int64_t seed, NetworkGameInitData *initData, DW return false; } - if (!onlineMode) { + if (authProvider == "offline") { logger.warning("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!"); logger.warning("The server will make no attempt to authenticate usernames. Beware."); logger.warning("While this makes the game possible to play without internet access, it also opens up the ability for hackers to connect with any username they choose."); - logger.warning("To change this, set \"online-mode\" to \"true\" in the server.settings file."); + logger.warning("To change this, set \"auth-provider\" to \"mojang\" or \"elyby\" in server.properties."); } #endif setPlayers(new PlayerList(this)); diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index 1ed5db9d7c..6129a8ca49 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -112,7 +112,7 @@ class MinecraftServer : public ConsoleInputSource vector consoleInput; // 4J - was synchronizedList - TODO - investigate CRITICAL_SECTION m_consoleInputCS; public: - bool onlineMode; + std::string authProvider; // mojang, elyby, or offline. not recommended. bool animals; bool npcs; bool pvp; diff --git a/Minecraft.Client/PendingConnection.cpp b/Minecraft.Client/PendingConnection.cpp index f24086c1b6..cac80662a8 100644 --- a/Minecraft.Client/PendingConnection.cpp +++ b/Minecraft.Client/PendingConnection.cpp @@ -13,7 +13,15 @@ #include "..\Minecraft.World\net.minecraft.world.level.storage.h" #include "..\Minecraft.World\net.minecraft.world.item.h" #include "..\Minecraft.World\SharedConstants.h" +#include "..\Minecraft.World\GameUUID.h" +#include "..\Minecraft.World\AuthSchemePacket.h" +#include "..\Minecraft.World\AuthResponsePacket.h" +#include "..\Minecraft.World\AuthResultPacket.h" #include "Settings.h" +#include +#ifdef _WINDOWS64 +#include "..\newauth\include\newauth.h" +#endif #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "..\Minecraft.Server\ServerLogManager.h" #include "..\Minecraft.Server\Access\Access.h" @@ -63,6 +71,8 @@ PendingConnection::PendingConnection(MinecraftServer *server, Socket *socket, co PendingConnection::~PendingConnection() { + if (m_authVerifyResult) + m_authVerifyResult->cancelled.store(true, std::memory_order_release); delete connection; } @@ -73,6 +83,71 @@ void PendingConnection::tick() this->handleAcceptedLogin(acceptedLogin); acceptedLogin = nullptr; } + + if (m_authState == eAuth_Verifying && m_authVerifyResult && + m_authVerifyResult->ready.load(std::memory_order_acquire)) + { + auto verifyResult = m_authVerifyResult; + bool authSuccess = false; + std::string uuid; + std::string uname; + std::vector skinData; + std::string skinUuid; + std::string errMsg; + + { + std::lock_guard lock(verifyResult->mutex); + authSuccess = verifyResult->success; + if (authSuccess) + { + uuid = verifyResult->uuid; + uname = verifyResult->username; + skinData = std::move(verifyResult->skinData); + skinUuid = verifyResult->skinUuid; + } + else + { + errMsg = verifyResult->errorMessage; + } + } + + m_authVerifyResult.reset(); + + if (authSuccess) + { + if (m_authAssignedUuid.empty()) + { + m_authAssignedUuid = std::wstring(uuid.begin(), uuid.end()); + m_authAssignedUsername = std::wstring(uname.begin(), uname.end()); + } + + std::string skinKeyUuid = skinUuid.empty() ? uuid : skinUuid; + std::string skinKey; +#ifdef _WINDOWS64 + if (server->authProvider == "elyby") + skinKey = newauth::MakeElybySkinKey(skinKeyUuid); + else + skinKey = newauth::MakeSkinKey(skinKeyUuid); +#endif + m_authSkinKey = std::wstring(skinKey.begin(), skinKey.end()); + m_authSkinData = std::move(skinData); + + name = m_authAssignedUsername; + + send(std::make_shared(true, m_authAssignedUuid, + m_authAssignedUsername, L"", m_authSkinKey, m_authSkinData)); + m_authState = eAuth_WaitingAck; + app.DebugPrintf("Auth: Verification succeeded for %ls\n", m_authAssignedUsername.c_str()); + } + else + { + std::wstring wErrMsg(errMsg.begin(), errMsg.end()); + app.DebugPrintf("Auth: Verification failed: %s\n", errMsg.c_str()); + send(std::make_shared(false, L"", L"", wErrMsg)); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + } + } + if (_tick++ == MAX_TICKS_BEFORE_LOGIN) { disconnect(DisconnectPacket::eDisconnect_LoginTooLong); @@ -150,8 +225,8 @@ void PendingConnection::sendPreLoginResponse() } } -#if 0 - if (false)// server->onlineMode) // 4J - removed + #if 0 + if (false) // 4J - removed { loginKey = L"TOIMPLEMENT"; // 4J - todo Long.toHexString(random.nextLong()); connection->send( shared_ptr( new PreLoginPacket(loginKey, ugcXuids, ugcXuidCount, ugcFriendsOnlyBits, server->m_ugcPlayersVersion, szUniqueMapName,app.GetGameHostOption(eGameHostOption_All),hostIndex) ) ); @@ -163,10 +238,259 @@ void PendingConnection::sendPreLoginResponse() BYTE cappedHostIndex = (hostIndex >= 255u) ? 254 : static_cast(hostIndex); connection->send(std::make_shared(L"-", ugcXuids, cappedCount, ugcFriendsOnlyBits, server->m_ugcPlayersVersion, szUniqueMapName, app.GetGameHostOption(eGameHostOption_All), cappedHostIndex, server->m_texturePackId)); } + +#ifdef _WINDOWS64 + sendAuthScheme(); +#endif +} + +void PendingConnection::sendAuthScheme() +{ + std::string hexId = GameUUID::generateV4().toUndashed(); + if (hexId.size() > 20) hexId.resize(20); + m_authServerId = hexId; + + vector schemes; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (server->authProvider == "elyby") + { + schemes.push_back(L"elyby"); + } + else if (server->authProvider == "offline") + { + schemes.push_back(L"offline"); + } + else + { + schemes.push_back(L"mojang"); + } + #else + { + schemes.push_back(L"mojang"); + schemes.push_back(L"elyby"); + schemes.push_back(L"offline"); + } + #endif + + wstring wServerId(m_authServerId.begin(), m_authServerId.end()); + send(std::make_shared(schemes, wServerId)); + m_authState = eAuth_WaitingResponse; + + app.DebugPrintf("Auth: Sent AuthSchemePacket with %d scheme(s), serverId=%s\n", + (int)schemes.size(), m_authServerId.c_str()); +} + +void PendingConnection::handleAuthResponse(shared_ptr packet) +{ + if (m_authState != eAuth_WaitingResponse) + { + app.DebugPrintf("Auth: Received AuthResponsePacket in wrong state %d\n", m_authState); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } + + std::string chosenScheme(packet->chosenScheme.begin(), packet->chosenScheme.end()); + std::string clientUsername(packet->username.begin(), packet->username.end()); + std::string clientUuid(packet->mojangUuid.begin(), packet->mojangUuid.end()); + + app.DebugPrintf("Auth: Client chose scheme '%s', username='%s'\n", + chosenScheme.c_str(), clientUsername.c_str()); + + if (chosenScheme == "offline") + { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + bool offlineAllowed = (server->authProvider == "offline"); +#else + bool offlineAllowed = true; +#endif + if (!offlineAllowed) + { + send(std::make_shared(false, L"", L"", + L"Server does not allow offline authentication")); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } + +#if !defined(MINECRAFT_SERVER_BUILD) + // assumed that offline/singleplayer or lan worlds will auto-trust uuids. + std::string uuid = clientUuid; + if (uuid.empty()) + { + GameUUID offlineUuid = GameUUID::generateOffline(clientUsername); + uuid = offlineUuid.toDashed(); + } + + m_authAssignedUuid = std::wstring(uuid.begin(), uuid.end()); + m_authAssignedUsername = std::wstring(clientUsername.begin(), clientUsername.end()); + name = m_authAssignedUsername; + + send(std::make_shared(true, m_authAssignedUuid, + m_authAssignedUsername, L"")); + m_authState = eAuth_WaitingAck; + app.DebugPrintf("Auth: Offline auth accepted (single-player), uuid=%s\n", uuid.c_str()); + return; +#else + // for ded server to regenerate uuid serverside to prevent uuid forgery + GameUUID offlineUuid = GameUUID::generateOffline(clientUsername); + std::string dashedUuid = offlineUuid.toDashed(); + + m_authAssignedUuid = std::wstring(dashedUuid.begin(), dashedUuid.end()); + m_authAssignedUsername = std::wstring(clientUsername.begin(), clientUsername.end()); + name = m_authAssignedUsername; + if (!clientUuid.empty()) + { + m_authState = eAuth_Verifying; + m_authVerifyResult = std::make_shared(); + m_authVerifyResult->success = true; + m_authVerifyResult->username = clientUsername; + m_authVerifyResult->uuid = dashedUuid; + m_authVerifyResult->skinUuid = clientUuid; // just use the user's uuid if auth'd with mojang or ely.by. ez. + + std::shared_ptr result = m_authVerifyResult; + std::string fetchUuid = clientUuid; + + std::thread([result, fetchUuid]() { + try { + if (result->cancelled.load(std::memory_order_acquire)) + return; + std::string fetchErr; + std::string skinUrl = newauth::FetchProfileSkinUrl(fetchUuid, fetchErr); + if (!skinUrl.empty()) + { + auto skinPng = newauth::FetchSkinPng(skinUrl, fetchErr); + if (!skinPng.empty() && newauth::ValidateSkinPng(skinPng.data(), skinPng.size())) + { + std::lock_guard lock(result->mutex); + result->skinUrl = skinUrl; + result->skinData = std::move(skinPng); + } + } + result->ready.store(true, std::memory_order_release); + } catch (...) { + result->ready.store(true, std::memory_order_release); + } + }).detach(); + return; + } + + send(std::make_shared(true, m_authAssignedUuid, + m_authAssignedUsername, L"")); + m_authState = eAuth_WaitingAck; + app.DebugPrintf("Auth: Offline auth success, uuid=%s\n", dashedUuid.c_str()); + return; +#endif + } + + if (chosenScheme == "mojang" || chosenScheme == "elyby") + { + bool isElyby = (chosenScheme == "elyby"); + +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (server->authProvider == "offline") + { + send(std::make_shared(false, L"", L"", + L"Server does not allow online authentication")); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } + + bool wantElyby = (server->authProvider == "elyby"); + if (isElyby != wantElyby) + { + app.DebugPrintf("Auth: Client chose '%s' but server expects '%s'\n", + chosenScheme.c_str(), server->authProvider.c_str()); + send(std::make_shared(false, L"", L"", + L"Auth scheme mismatch")); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } + #endif + + m_authState = eAuth_Verifying; + m_authVerifyResult = std::make_shared(); + + std::string serverId = m_authServerId; + std::string username = clientUsername; + std::shared_ptr result = m_authVerifyResult; + +#ifdef _WINDOWS64 + std::thread([result, username, serverId, isElyby]() { + try { + if (result->cancelled.load(std::memory_order_acquire)) + return; + + std::string error; + newauth::HasJoinedResult hjResult; + + if (isElyby) + hjResult = newauth::ElybyHasJoined(username, serverId, error); + else + hjResult = newauth::HasJoined(username, serverId, error); + + if (result->cancelled.load(std::memory_order_acquire)) + return; + + std::vector skin; + if (hjResult.success && !hjResult.skinUrl.empty()) + { + std::string skinErr; + skin = newauth::FetchSkinPng(hjResult.skinUrl, skinErr); + if (!skin.empty() && !newauth::ValidateSkinPng(skin.data(), skin.size())) + skin.clear(); + } + + { + std::lock_guard lock(result->mutex); + if (hjResult.success) + { + result->success = true; + result->username = hjResult.username; + result->uuid = hjResult.uuid; + result->skinUrl = hjResult.skinUrl; + if (!skin.empty()) + result->skinData = std::move(skin); + } + else + { + result->success = false; + result->errorMessage = error.empty() ? "Authentication failed" : error; + } + } + result->ready.store(true, std::memory_order_release); + } catch (...) { + { + std::lock_guard lock(result->mutex); + result->success = false; + result->errorMessage = "Internal auth error"; + } + result->ready.store(true, std::memory_order_release); + } + }).detach(); +#endif + return; + } + + // only really a problem if the client is modified. You Know Who You + app.DebugPrintf("Auth: Unknown scheme '%s'\n", chosenScheme.c_str()); + send(std::make_shared(false, L"", L"", L"Unknown auth scheme")); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); } void PendingConnection::handleLogin(shared_ptr packet) { +#ifdef _WINDOWS64 + if (m_authState != eAuth_WaitingAck && m_authState != eAuth_Done) + { + app.DebugPrintf("Auth: LoginPacket received in state %d, rejecting\n", m_authState); + disconnect(DisconnectPacket::eDisconnect_AuthFailed); + return; + } + if (m_authState == eAuth_WaitingAck) + { + m_authState = eAuth_Done; + } +#endif + // printf("Server: handleLogin\n"); //name = packet->userName; if (packet->clientVersion != SharedConstants::NETWORK_PROTOCOL_VERSION) @@ -183,11 +507,8 @@ void PendingConnection::handleLogin(shared_ptr packet) return; } - //if (true)// 4J removed !server->onlineMode) bool sentDisconnect = false; - // Use the same Xuid choice as handleAcceptedLogin (offline first, online fallback). - // PlayerUID loginXuid = packet->m_offlineXuid; if (loginXuid == INVALID_XUID) loginXuid = packet->m_onlineXuid; @@ -336,6 +657,22 @@ void PendingConnection::handleAcceptedLogin(shared_ptr packet) #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) ServerRuntime::ServerLogManager::OnAcceptedPlayerLogin(GetPendingConnectionSmallId(connection), name); #endif + +#ifdef _WINDOWS64 + // Apply auth-verified skin to the player so that placeNewPlayer can + // distribute it to all clients via the existing texture mechanism. + if (!m_authSkinKey.empty() && !m_authSkinData.empty()) + { + DWORD skinBytes = (DWORD)m_authSkinData.size(); + PBYTE skinCopy = new BYTE[skinBytes]; + memcpy(skinCopy, m_authSkinData.data(), skinBytes); + app.AddMemoryTextureFile(m_authSkinKey, skinCopy, skinBytes); + playerEntity->customTextureUrl = m_authSkinKey; + app.DebugPrintf("Auth: Registered server-side skin '%ls' (%d bytes) for %ls\n", + m_authSkinKey.c_str(), (int)skinBytes, name.c_str()); + } +#endif + server->getPlayers()->placeNewPlayer(connection, playerEntity, packet); 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 } diff --git a/Minecraft.Client/PendingConnection.h b/Minecraft.Client/PendingConnection.h index e8a493b097..7cf378c42d 100644 --- a/Minecraft.Client/PendingConnection.h +++ b/Minecraft.Client/PendingConnection.h @@ -1,14 +1,43 @@ #pragma once +#include +#include +#include #include "..\Minecraft.World\PacketListener.h" class MinecraftServer; class Socket; class LoginPacket; +class AuthSchemePacket; +class AuthResponsePacket; class Connection; class Random; using namespace std; class PendingConnection : public PacketListener { +public: + enum eAuthState + { + eAuth_None = 0, + eAuth_WaitingResponse, + eAuth_Verifying, + eAuth_WaitingAck, + eAuth_Done + }; + + struct AuthVerifyResult + { + std::atomic ready{false}; + std::atomic cancelled{false}; + std::mutex mutex; + bool success = false; + std::string username; + std::string uuid; + std::string skinUuid; // UUID to use for skin key (may differ from uuid for offline auth) + std::string skinUrl; + std::vector skinData; + std::string errorMessage; + }; + private: static const int FAKE_LAG = 0; static const int MAX_TICKS_BEFORE_LOGIN = 20 * 30; @@ -27,6 +56,14 @@ class PendingConnection : public PacketListener shared_ptr acceptedLogin; wstring loginKey; + eAuthState m_authState = eAuth_None; + std::string m_authServerId; + std::wstring m_authAssignedUuid; + std::wstring m_authAssignedUsername; + std::wstring m_authSkinKey; + std::vector m_authSkinData; + std::shared_ptr m_authVerifyResult; + public: PendingConnection(MinecraftServer *server, Socket *socket, const wstring& id); ~PendingConnection(); @@ -35,6 +72,7 @@ class PendingConnection : public PacketListener virtual void handlePreLogin(shared_ptr packet); virtual void handleLogin(shared_ptr packet); virtual void handleAcceptedLogin(shared_ptr packet); + virtual void handleAuthResponse(shared_ptr packet); virtual void onDisconnect(DisconnectPacket::eDisconnectReason reason, void *reasonObjects); virtual void handleGetInfo(shared_ptr packet); virtual void handleKeepAlive(shared_ptr packet); @@ -46,4 +84,5 @@ class PendingConnection : public PacketListener private: void sendPreLoginResponse(); + void sendAuthScheme(); }; \ No newline at end of file diff --git a/Minecraft.Client/Textures.h b/Minecraft.Client/Textures.h index 1fca56106a..976679843e 100644 --- a/Minecraft.Client/Textures.h +++ b/Minecraft.Client/Textures.h @@ -290,6 +290,9 @@ class Textures public: void clearLastBoundId(); + // i guess the nativeui wasn't a terrible idea. it just needed to be written right. + int loadTextureByPath(const wstring& path) { return loadTexture(TN_COUNT, path); } + private: int loadTexture(TEXTURE_NAME texId, const wstring& resourceName); public: diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp index acc043e5fc..e86bac1156 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.cpp @@ -1317,6 +1317,7 @@ bool WinsockNetLayer::StartAdvertising(int gamePort, const wchar_t* hostName, un s_advertiseData.texturePackParentId = texPackId; s_advertiseData.subTexturePackId = subTexId; s_advertiseData.isJoinable = 0; + s_advertiseData.isDedicatedServer = g_Win64DedicatedServer ? 1 : 0; s_hostGamePort = gamePort; LeaveCriticalSection(&s_advertiseLock); @@ -1519,6 +1520,7 @@ DWORD WINAPI WinsockNetLayer::DiscoveryThreadProc(LPVOID param) s_discoveredSessions[i].texturePackParentId = broadcast->texturePackParentId; s_discoveredSessions[i].subTexturePackId = broadcast->subTexturePackId; s_discoveredSessions[i].isJoinable = (broadcast->isJoinable != 0); + s_discoveredSessions[i].isDedicatedServer = (broadcast->isDedicatedServer != 0); s_discoveredSessions[i].lastSeenTick = now; found = true; break; @@ -1539,6 +1541,7 @@ DWORD WINAPI WinsockNetLayer::DiscoveryThreadProc(LPVOID param) session.texturePackParentId = broadcast->texturePackParentId; session.subTexturePackId = broadcast->subTexturePackId; session.isJoinable = (broadcast->isJoinable != 0); + session.isDedicatedServer = (broadcast->isDedicatedServer != 0); session.lastSeenTick = now; s_discoveredSessions.push_back(session); diff --git a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h index 8a11e391dd..749e2f824e 100644 --- a/Minecraft.Client/Windows64/Network/WinsockNetLayer.h +++ b/Minecraft.Client/Windows64/Network/WinsockNetLayer.h @@ -36,6 +36,7 @@ struct Win64LANBroadcast DWORD texturePackParentId; BYTE subTexturePackId; BYTE isJoinable; + BYTE isDedicatedServer; }; #pragma pack(pop) @@ -51,6 +52,7 @@ struct Win64LANSession unsigned int texturePackParentId; unsigned char subTexturePackId; bool isJoinable; + bool isDedicatedServer; DWORD lastSeenTick; }; diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index fa5f4cccbd..58c5bd094a 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -49,6 +49,7 @@ #include "Network\WinsockNetLayer.h" #include "Windows64_Xuid.h" #include "Common/UI/UI.h" +#include "..\\..\\newauth\\include\\newauthManager.h" // Forward-declare the internal Renderer class and its global instance from 4J_Render_PC_d.lib. // C4JRender (RenderManager) is a stateless wrapper — all D3D state lives in InternalRenderManager. @@ -1276,6 +1277,8 @@ static Minecraft* InitialiseMinecraftRuntime() ProfileManager.SetDebugFullOverride(true); + newauthManager::Get().TryRestoreActiveJavaAccount(); + Tesselator::CreateNewThreadStorage(1024 * 1024); AABB::CreateNewThreadStorage(); Vec3::CreateNewThreadStorage(); diff --git a/Minecraft.Client/cmake/sources/Windows.cmake b/Minecraft.Client/cmake/sources/Windows.cmake index 7fc07abd89..4a849a2171 100644 --- a/Minecraft.Client/cmake/sources/Windows.cmake +++ b/Minecraft.Client/cmake/sources/Windows.cmake @@ -28,6 +28,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIGroup.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UILayer.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UILayer.h" + "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/NativeUIRenderer.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/NativeUIRenderer.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UITTFFont.cpp" @@ -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_MainMenu.cpp" "${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.h" "${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_SaveMessage.cpp" diff --git a/Minecraft.Server/CMakeLists.txt b/Minecraft.Server/CMakeLists.txt index 52e5826eb4..7b25a40ad0 100644 --- a/Minecraft.Server/CMakeLists.txt +++ b/Minecraft.Server/CMakeLists.txt @@ -37,6 +37,7 @@ set_target_properties(Minecraft.Server PROPERTIES target_link_libraries(Minecraft.Server PRIVATE Minecraft.World + newauth d3d11 d3dcompiler XInput9_1_0 diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index d6ba64e7e2..0880bf8c94 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -80,7 +80,8 @@ static const ServerPropertyDefault kServerPropertyDefaults[] = { "spawn-monsters", "true" }, { "spawn-npcs", "true" }, { "tnt", "true" }, - { "trust-players", "true" } + { "trust-players", "true" }, + { "auth-provider", "mojang" } }; static std::string BoolToString(bool value) @@ -783,6 +784,11 @@ ServerPropertiesConfig LoadServerPropertiesConfig() merged[it->first] = it->second; } + if (merged.erase("online-mode") > 0) + { + shouldWrite = true; + } + std::string worldName = TrimAscii(merged["level-name"]); if (worldName.empty()) { @@ -904,6 +910,8 @@ bool SaveServerPropertiesConfig(const ServerPropertiesConfig &config) } } + merged.erase("online-mode"); + std::string worldName = TrimAscii(WideToUtf8(config.worldName)); if (worldName.empty()) { diff --git a/Minecraft.World/AuthResponsePacket.cpp b/Minecraft.World/AuthResponsePacket.cpp new file mode 100644 index 0000000000..3f153934e1 --- /dev/null +++ b/Minecraft.World/AuthResponsePacket.cpp @@ -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); + + if (chosenScheme != L"mojang" && chosenScheme != L"offline" && chosenScheme != L"elyby") + chosenScheme = L""; + + // only allow normal mc username characters + 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(3 * sizeof(short) + + (chosenScheme.length() + mojangUuid.length() + username.length()) * sizeof(wchar_t)); +} diff --git a/Minecraft.World/AuthResponsePacket.h b/Minecraft.World/AuthResponsePacket.h new file mode 100644 index 0000000000..5f8e59929e --- /dev/null +++ b/Minecraft.World/AuthResponsePacket.h @@ -0,0 +1,22 @@ +#pragma once +#include "Packet.h" +using namespace std; + +class AuthResponsePacket : public Packet, public enable_shared_from_this +{ +public: + wstring chosenScheme; + wstring mojangUuid; + wstring 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 create() { return make_shared(); } + virtual int getId() { return 171; } +}; diff --git a/Minecraft.World/AuthResultPacket.cpp b/Minecraft.World/AuthResultPacket.cpp new file mode 100644 index 0000000000..45d9dbbaba --- /dev/null +++ b/Minecraft.World/AuthResultPacket.cpp @@ -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 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 the skin blob (length + bytes) + // cap at 32kb, a real skin png is like 4kb tops + int skinSize = dis->readInt(); + if (skinSize > 0 && skinSize <= 32768) + { + skinData.resize(static_cast(skinSize)); + for (int i = 0; i < skinSize; i++) + skinData[i] = dis->readByte(); + } + else + { + skinData.clear(); + // eat the bytes anyway so the stream doesnt get fucked up + 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(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(sizeof(bool) + 4 * sizeof(short) + + (assignedUuid.length() + assignedUsername.length() + errorMessage.length() + skinKey.length()) * sizeof(wchar_t) + + sizeof(int) + skinData.size()); +} diff --git a/Minecraft.World/AuthResultPacket.h b/Minecraft.World/AuthResultPacket.h new file mode 100644 index 0000000000..13a8262185 --- /dev/null +++ b/Minecraft.World/AuthResultPacket.h @@ -0,0 +1,28 @@ +#pragma once +#include "Packet.h" +#include +using namespace std; + +class AuthResultPacket : public Packet, public enable_shared_from_this +{ +public: + bool success; + wstring assignedUuid; + wstring assignedUsername; + wstring errorMessage; + wstring skinKey; + std::vector skinData; + + AuthResultPacket(); + AuthResultPacket(bool success, const wstring& assignedUuid, const wstring& assignedUsername, + const wstring& errorMessage, const wstring& skinKey = L"", + std::vector skinData = {}); + + virtual void read(DataInputStream *dis); + virtual void write(DataOutputStream *dos); + virtual void handle(PacketListener *listener); + virtual int getEstimatedSize(); + + static shared_ptr create() { return make_shared(); } + virtual int getId() { return 172; } +}; diff --git a/Minecraft.World/AuthSchemePacket.cpp b/Minecraft.World/AuthSchemePacket.cpp new file mode 100644 index 0000000000..5c03bd4455 --- /dev/null +++ b/Minecraft.World/AuthSchemePacket.cpp @@ -0,0 +1,56 @@ +#include "stdafx.h" +#include "InputOutputStream.h" +#include "PacketListener.h" +#include "AuthSchemePacket.h" + +AuthSchemePacket::AuthSchemePacket() +{ +} + +AuthSchemePacket::AuthSchemePacket(const vector& 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; // dont let someone send us a billion schemes + schemes.clear(); + schemes.reserve(static_cast(count)); + for (int i = 0; i < count; i++) + { + wstring scheme = readUtf(dis, 32); + // only take scheme names we actually know about + 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(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(s.length()) * sizeof(wchar_t); + } + size += sizeof(short) + static_cast(serverId.length()) * sizeof(wchar_t); + return size; +} diff --git a/Minecraft.World/AuthSchemePacket.h b/Minecraft.World/AuthSchemePacket.h new file mode 100644 index 0000000000..4fc04ad231 --- /dev/null +++ b/Minecraft.World/AuthSchemePacket.h @@ -0,0 +1,22 @@ +#pragma once +#include "Packet.h" +using namespace std; + +class AuthSchemePacket : public Packet, public enable_shared_from_this +{ +public: + // "mojang", "offline" etc + vector schemes; + wstring serverId; // hex challenge (20 chars), empty for offline + + AuthSchemePacket(); + AuthSchemePacket(const vector& 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 create() { return make_shared(); } + virtual int getId() { return 170; } +}; diff --git a/Minecraft.World/DisconnectPacket.h b/Minecraft.World/DisconnectPacket.h index 3c96a429b1..c98b51e9b0 100644 --- a/Minecraft.World/DisconnectPacket.h +++ b/Minecraft.World/DisconnectPacket.h @@ -50,6 +50,7 @@ class DisconnectPacket : public Packet, public enable_shared_from_this +#include +#include + +#ifdef _WIN32 +#include +#include +#pragma comment(lib, "bcrypt.lib") +#endif + +static uint8_t hexVal(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; +} + +std::string GameUUID::toDashed() const +{ + char buf[37]; + sprintf_s(buf, sizeof(buf), + "%08x-%04x-%04x-%04x-%012llx", + (unsigned int)(hi >> 32), + (unsigned int)((hi >> 16) & 0xFFFF), + (unsigned int)(hi & 0xFFFF), + (unsigned int)(lo >> 48), + (unsigned long long)(lo & 0x0000FFFFFFFFFFFFULL)); + return std::string(buf); +} + +std::string GameUUID::toUndashed() const +{ + char buf[33]; + sprintf_s(buf, sizeof(buf), + "%016llx%016llx", + (unsigned long long)hi, + (unsigned long long)lo); + return std::string(buf); +} + +std::wstring GameUUID::toWDashed() const +{ + std::string s = toDashed(); + return std::wstring(s.begin(), s.end()); +} + +GameUUID GameUUID::fromDashed(const std::string& s) +{ + std::string undashed; + undashed.reserve(32); + for (size_t i = 0; i < s.size(); i++) + { + if (s[i] != '-') + undashed.push_back(s[i]); + } + return fromUndashed(undashed); +} + +GameUUID GameUUID::fromUndashed(const std::string& s) +{ + GameUUID uuid; + if (s.size() < 32) + return uuid; + + uuid.hi = 0; + for (int i = 0; i < 16; i++) + { + uuid.hi = (uuid.hi << 4) | hexVal(s[i]); + } + uuid.lo = 0; + for (int i = 16; i < 32; i++) + { + uuid.lo = (uuid.lo << 4) | hexVal(s[i]); + } + return uuid; +} +GameUUID GameUUID::generateV4() +{ + GameUUID uuid; + uint8_t bytes[16] = {}; + +#ifdef _WIN32 + NTSTATUS status = BCryptGenRandom(NULL, bytes, sizeof(bytes), BCRYPT_USE_SYSTEM_PREFERRED_RNG); + if (!BCRYPT_SUCCESS(status)) + { + for (int i = 0; i < 16; i++) bytes[i] = (uint8_t)(rand() & 0xFF); + } +#else + static uint64_t counter = 0; + uint64_t seed = (uint64_t)time(NULL) ^ (++counter * 6364136223846793005ULL); + for (int i = 0; i < 16; i++) + { + seed = seed * 6364136223846793005ULL + 1442695040888963407ULL; + bytes[i] = (uint8_t)(seed >> 56); + } +#endif + + // Set version 4 (bits 48-51 of hi = 0100) + bytes[6] = (bytes[6] & 0x0F) | 0x40; + // Set variant 1 (bits 0-1 of byte 8 = 10) + bytes[8] = (bytes[8] & 0x3F) | 0x80; + + uuid.hi = 0; + for (int i = 0; i < 8; i++) + uuid.hi = (uuid.hi << 8) | bytes[i]; + + uuid.lo = 0; + for (int i = 8; i < 16; i++) + uuid.lo = (uuid.lo << 8) | bytes[i]; + + return uuid; +} + +namespace +{ + struct MD5State + { + uint32_t state[4]; + uint64_t count; + uint8_t buffer[64]; + }; + + static const uint32_t md5_T[64] = { + 0xd76aa478,0xe8c7b756,0x242070db,0xc1bdceee,0xf57c0faf,0x4787c62a,0xa8304613,0xfd469501, + 0x698098d8,0x8b44f7af,0xffff5bb1,0x895cd7be,0x6b901122,0xfd987193,0xa679438e,0x49b40821, + 0xf61e2562,0xc040b340,0x265e5a51,0xe9b6c7aa,0xd62f105d,0x02441453,0xd8a1e681,0xe7d3fbc8, + 0x21e1cde6,0xc33707d6,0xf4d50d87,0x455a14ed,0xa9e3e905,0xfcefa3f8,0x676f02d9,0x8d2a4c8a, + 0xfffa3942,0x8771f681,0x6d9d6122,0xfde5380c,0xa4beea44,0x4bdecfa9,0xf6bb4b60,0xbebfbc70, + 0x289b7ec6,0xeaa127fa,0xd4ef3085,0x04881d05,0xd9d4d039,0xe6db99e5,0x1fa27cf8,0xc4ac5665, + 0xf4292244,0x432aff97,0xab9423a7,0xfc93a039,0x655b59c3,0x8f0ccc92,0xffeff47d,0x85845dd1, + 0x6fa87e4f,0xfe2ce6e0,0xa3014314,0x4e0811a1,0xf7537e82,0xbd3af235,0x2ad7d2bb,0xeb86d391 + }; + + static const int md5_S[64] = { + 7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22, + 5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20, + 4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23, + 6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21 + }; + + static inline uint32_t rotl32(uint32_t x, int n) { return (x << n) | (x >> (32 - n)); } + + static void md5_transform(uint32_t state[4], const uint8_t block[64]) + { + uint32_t M[16]; + for (int i = 0; i < 16; i++) + M[i] = (uint32_t)block[i * 4] | ((uint32_t)block[i * 4 + 1] << 8) | + ((uint32_t)block[i * 4 + 2] << 16) | ((uint32_t)block[i * 4 + 3] << 24); + + uint32_t a = state[0], b = state[1], c = state[2], d = state[3]; + + for (int i = 0; i < 64; i++) + { + uint32_t f; + int g; + if (i < 16) { f = (b & c) | (~b & d); g = i; } + else if (i < 32) { f = (d & b) | (~d & c); g = (5 * i + 1) % 16; } + else if (i < 48) { f = b ^ c ^ d; g = (3 * i + 5) % 16; } + else { f = c ^ (b | ~d); g = (7 * i) % 16; } + + uint32_t temp = d; + d = c; + c = b; + b = b + rotl32(a + f + md5_T[i] + M[g], md5_S[i]); + a = temp; + } + + state[0] += a; state[1] += b; state[2] += c; state[3] += d; + } + + static void md5(const uint8_t* data, size_t len, uint8_t digest[16]) + { + uint32_t state[4] = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 }; + + size_t i = 0; + for (; i + 64 <= len; i += 64) + md5_transform(state, data + i); + + uint8_t block[64] = {}; + size_t remain = len - i; + if (remain > 0) + memcpy(block, data + i, remain); + + block[remain] = 0x80; + + if (remain >= 56) + { + md5_transform(state, block); + memset(block, 0, 64); + } + + uint64_t bitLen = (uint64_t)len * 8; + for (int b = 0; b < 8; b++) + block[56 + b] = (uint8_t)(bitLen >> (b * 8)); + + md5_transform(state, block); + + for (int b = 0; b < 4; b++) + { + digest[b * 4 + 0] = (uint8_t)(state[b]); + digest[b * 4 + 1] = (uint8_t)(state[b] >> 8); + digest[b * 4 + 2] = (uint8_t)(state[b] >> 16); + digest[b * 4 + 3] = (uint8_t)(state[b] >> 24); + } + } +} + +GameUUID GameUUID::generateOffline(const std::string& playerName) +{ + std::string input = "OfflinePlayer:" + playerName; + + uint8_t digest[16]; + md5((const uint8_t*)input.c_str(), input.size(), digest); + digest[6] = (digest[6] & 0x0F) | 0x30; + digest[8] = (digest[8] & 0x3F) | 0x80; + GameUUID uuid; + uuid.hi = 0; + for (int i = 0; i < 8; i++) + uuid.hi = (uuid.hi << 8) | digest[i]; + uuid.lo = 0; + for (int i = 8; i < 16; i++) + uuid.lo = (uuid.lo << 8) | digest[i]; + + return uuid; +} diff --git a/Minecraft.World/GameUUID.h b/Minecraft.World/GameUUID.h new file mode 100644 index 0000000000..bfd4eaa7f9 --- /dev/null +++ b/Minecraft.World/GameUUID.h @@ -0,0 +1,45 @@ +#pragma once +#include +#include +#include +#include + +struct GameUUID { + uint64_t hi = 0; + uint64_t lo = 0; + bool isValid() const { return hi != 0 || lo != 0; } + std::string toDashed() const; + std::string toUndashed() const; + std::wstring toWDashed() const; + static GameUUID fromDashed(const std::string& s); + static GameUUID fromUndashed(const std::string& s); + static GameUUID generateV4(); + static GameUUID generateOffline(const std::string& playerName); + bool operator==(const GameUUID& o) const { return hi == o.hi && lo == o.lo; } + bool operator!=(const GameUUID& o) const { return !(*this == o); } + bool operator<(const GameUUID& o) const { return hi < o.hi || (hi == o.hi && lo < o.lo); } + + struct Hash { + size_t operator()(const GameUUID& u) const { + size_t h = std::hash{}(u.hi); + h ^= std::hash{}(u.lo) + 0x9e3779b9 + (h << 6) + (h >> 2); + return h; + } + }; +}; +inline const GameUUID INVALID_UUID = {}; +inline std::wistream& operator>>(std::wistream& is, GameUUID& uuid) +{ + std::wstring ws; + is >> ws; + std::string s(ws.begin(), ws.end()); + uuid = GameUUID::fromDashed(s); + return is; +} +namespace std { + template<> struct hash { + size_t operator()(const GameUUID& u) const { + return GameUUID::Hash{}(u); + } + }; +} diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 05bf932dcb..f644467009 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -131,6 +131,10 @@ void Packet::staticCtor() map(166, true, true, false, false, typeid(XZPacket), XZPacket::create); map(167, false, true, false, false, typeid(GameCommandPacket), GameCommandPacket::create); + map(170, true, false, true, false, typeid(AuthSchemePacket), AuthSchemePacket::create); // Server -> Client + map(171, false, true, false, false, typeid(AuthResponsePacket), AuthResponsePacket::create); // Client -> Server + map(172, true, false, true, false, typeid(AuthResultPacket), AuthResultPacket::create); // Server -> Client + map(200, true, false, true, false, typeid(AwardStatPacket), AwardStatPacket::create); map(201, true, true, false, false, typeid(PlayerInfoPacket), PlayerInfoPacket::create); // TODO New for 1.8.2 - Repurposed by 4J map(202, true, true, true, false, typeid(PlayerAbilitiesPacket), PlayerAbilitiesPacket::create); diff --git a/Minecraft.World/PacketListener.cpp b/Minecraft.World/PacketListener.cpp index 09fbb05251..3b6695105b 100644 --- a/Minecraft.World/PacketListener.cpp +++ b/Minecraft.World/PacketListener.cpp @@ -491,3 +491,18 @@ void PacketListener::handleGameCommand(shared_ptr packet) { onUnhandledPacket( (shared_ptr ) packet); } + +void PacketListener::handleAuthScheme(shared_ptr packet) +{ + onUnhandledPacket( (shared_ptr ) packet); +} + +void PacketListener::handleAuthResponse(shared_ptr packet) +{ + onUnhandledPacket( (shared_ptr ) packet); +} + +void PacketListener::handleAuthResult(shared_ptr packet) +{ + onUnhandledPacket( (shared_ptr ) packet); +} diff --git a/Minecraft.World/PacketListener.h b/Minecraft.World/PacketListener.h index f63b6bc7fd..edc04b5b57 100644 --- a/Minecraft.World/PacketListener.h +++ b/Minecraft.World/PacketListener.h @@ -111,6 +111,9 @@ class KickPlayerPacket; class AdditionalModelPartsPacket; class XZPacket; class GameCommandPacket; +class AuthSchemePacket; +class AuthResponsePacket; +class AuthResultPacket; class PacketListener { @@ -227,4 +230,7 @@ class PacketListener virtual void handleKickPlayer(shared_ptr packet); virtual void handleXZ(shared_ptr packet); virtual void handleGameCommand(shared_ptr packet); + virtual void handleAuthScheme(shared_ptr packet); + virtual void handleAuthResponse(shared_ptr packet); + virtual void handleAuthResult(shared_ptr packet); }; diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 8a4d683303..3d01b52263 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -151,6 +151,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT "${CMAKE_CURRENT_SOURCE_DIR}/Direction.h" "${CMAKE_CURRENT_SOURCE_DIR}/Facing.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Facing.h" + "${CMAKE_CURRENT_SOURCE_DIR}/GameUUID.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/GameUUID.h" "${CMAKE_CURRENT_SOURCE_DIR}/Pos.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Pos.h" "${CMAKE_CURRENT_SOURCE_DIR}/SharedConstants.cpp" @@ -259,6 +261,12 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/AddPlayerPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AnimatePacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthResponsePacket.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthResponsePacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthResultPacket.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthResultPacket.h" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthSchemePacket.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/AuthSchemePacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/AwardStatPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/BlockRegionUpdatePacket.cpp" diff --git a/Minecraft.World/net.minecraft.network.packet.h b/Minecraft.World/net.minecraft.network.packet.h index c1d52d1473..9b46a3536e 100644 --- a/Minecraft.World/net.minecraft.network.packet.h +++ b/Minecraft.World/net.minecraft.network.packet.h @@ -108,4 +108,7 @@ #include "KickPlayerPacket.h" #include "XZPacket.h" #include "GameCommandPacket.h" +#include "AuthSchemePacket.h" +#include "AuthResponsePacket.h" +#include "AuthResultPacket.h" diff --git a/Minecraft.World/x64headers/extraX64.h b/Minecraft.World/x64headers/extraX64.h index 4af047b5b9..76936962a2 100644 --- a/Minecraft.World/x64headers/extraX64.h +++ b/Minecraft.World/x64headers/extraX64.h @@ -132,6 +132,10 @@ void XMemSet(void *a, int t, size_t s); void XMemSet128(void *a, int t, size_t s); void *XPhysicalAlloc(SIZE_T a, ULONG_PTR b, ULONG_PTR c, DWORD d); void XPhysicalFree(void *a); +class IQNetPlayer; +void Win64_SetJoinedDedicatedServer(bool isDedicated); +bool Win64_IsJoinedDedicatedServer(); +bool Win64_ShouldExposeSessionPlayer(IQNetPlayer *player); class DLCManager; diff --git a/newauth/CMakeLists.txt b/newauth/CMakeLists.txt new file mode 100644 index 0000000000..da79bb0f56 --- /dev/null +++ b/newauth/CMakeLists.txt @@ -0,0 +1,61 @@ +include(FetchContent) +FetchContent_Declare( + sqlite3 + URL "https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip" + URL_HASH SHA256=aa73d8748095808471deaa8e6f34aa700e37f2f787f4425744f53fdd15a89c40 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE +) +FetchContent_MakeAvailable(sqlite3) + +# build sqlite3 as a static library +add_library(sqlite3_lib STATIC "${sqlite3_SOURCE_DIR}/sqlite3.c") +target_include_directories(sqlite3_lib PUBLIC "${sqlite3_SOURCE_DIR}") +target_compile_definitions(sqlite3_lib PRIVATE SQLITE_THREADSAFE=1) +if(MSVC) + target_compile_options(sqlite3_lib PRIVATE /W0) +endif() +configure_compiler_target(sqlite3_lib) + +set(NEWAUTH_SOURCES + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthCrypto.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthCrypto.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthHttp.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthHttp.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthInternal.h" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthJava.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthManager.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthElyby.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthSession.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthDb.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/newauthDb.h" +) +source_group("src" FILES ${NEWAUTH_SOURCES}) + +set(NEWAUTH_HEADERS + "${CMAKE_CURRENT_SOURCE_DIR}/include/newauth.h" + "${CMAKE_CURRENT_SOURCE_DIR}/include/newauthManager.h" +) +source_group("include" FILES ${NEWAUTH_HEADERS}) + +add_library(newauth STATIC ${NEWAUTH_SOURCES} ${NEWAUTH_HEADERS}) + +target_include_directories(newauth + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/include" + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/src" +) + +target_compile_definitions(newauth PRIVATE + _LIB + $<$:_DEBUG> + _CRT_SECURE_NO_WARNINGS +) + +configure_compiler_target(newauth) + +target_link_libraries(newauth PRIVATE + winhttp + bcrypt + sqlite3_lib +) diff --git a/newauth/include/newauth.h b/newauth/include/newauth.h new file mode 100644 index 0000000000..83f77d8b40 --- /dev/null +++ b/newauth/include/newauth.h @@ -0,0 +1,163 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace newauth { + +enum class AuthErrorCode { + None = 0, + NetworkTimeout, + NetworkError, + HttpError, + InvalidCredentials, + TokenExpired, + ServerUnavailable, + RateLimited, + ProfileNotOwned, + Cancelled, + InternalError, +}; + +struct AuthError { + AuthErrorCode code = AuthErrorCode::None; + std::string message; + int httpStatus = 0; + + bool ok() const { return code == AuthErrorCode::None; } + explicit operator bool() const { return !ok(); } +}; + +static constexpr const char* JAVA_CLIENT_ID = "00000000402b5328"; + +static constexpr const char* MSA_SCOPE = "service::user.auth.xboxlive.com::MBI_SSL"; + +struct DeviceCodeInfo { + std::string userCode; + std::string verificationUri; + std::string directUri; + std::string deviceCode; + int64_t expiresMs; + int64_t intervalMs; +}; + +using DeviceCodeCallback = std::function; + +struct JavaSession { + std::string username; + std::string uuid; + std::string accessToken; + int64_t expireMs; +}; + +class JavaAuthManager { +public: + JavaAuthManager(); + ~JavaAuthManager(); + + JavaAuthManager(const JavaAuthManager&) = delete; + JavaAuthManager& operator=(const JavaAuthManager&) = delete; + + bool Login(DeviceCodeCallback onDeviceCode, + JavaSession& outSession, + std::string& error, + int timeoutSeconds = 300); + + bool Refresh(JavaSession& outSession, std::string& error); + + bool IsLoggedIn() const; + void Logout(); + + void RequestCancel(); + + bool SaveTokens(const std::string& filePath) const; + bool LoadTokens(const std::string& filePath); + + std::string SerializeTokens() const; + bool DeserializeTokens(const std::string& json); + +private: + struct Impl; + std::unique_ptr m_impl; +}; + +struct ElybyTokens { + std::string accessToken; + std::string clientToken; + std::string uuid; + std::string username; +}; + +bool ElybyLogin(const std::string& username, const std::string& password, + ElybyTokens& outTokens, std::string& error); + +bool ElybyRefresh(ElybyTokens& tokens, std::string& error); + +bool ElybyValidate(const std::string& accessToken, std::string& error); + +bool ElybyLoadTokens(const std::string& path, ElybyTokens& out); +bool ElybySaveTokens(const std::string& path, const ElybyTokens& tokens); + +std::string ElybySerializeTokens(const ElybyTokens& tokens); +bool ElybyDeserializeTokens(const std::string& json, ElybyTokens& out); + +std::string MakeSkinKey(const std::string& uuid); + +std::string UndashUuid(const std::string& dashed); + +std::string DashUuid(const std::string& undashed); + +std::string GenerateOfflineUuid(const std::string& username); + +struct Uuid128 { + uint64_t hi = 0; + uint64_t lo = 0; + bool isValid() const { return hi != 0 || lo != 0; } +}; +Uuid128 ParseUuid128(const std::string& dashed); + +bool JoinServer(const std::string& accessToken, + const std::string& undashedUuid, + const std::string& serverId, + std::string& error); + +struct HasJoinedResult { + bool success = false; + AuthError error; + std::string username; + std::string uuid; + std::string skinUrl; + std::string capeUrl; +}; + +HasJoinedResult HasJoined(const std::string& username, + const std::string& serverId, + std::string& error); + +std::vector FetchSkinPng(const std::string& url, std::string& error); + +std::string FetchProfileSkinUrl(const std::string& uuid, std::string& error); + +std::vector FetchSkinPngRaw(const std::string& url, std::string& error); + +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); + +static constexpr size_t kMaxSkinBytes = 32768; + +bool ValidateSkinPng(const uint8_t* data, size_t size); + +} diff --git a/newauth/include/newauthManager.h b/newauth/include/newauthManager.h new file mode 100644 index 0000000000..d2f9382ed3 --- /dev/null +++ b/newauth/include/newauthManager.h @@ -0,0 +1,136 @@ +#pragma once + +#include "newauth.h" +#include +#include +#include +#include +#include +#include +#include + +#ifndef XUSER_MAX_COUNT +#define XUSER_MAX_COUNT 4 +#endif + +class newauthManager { +public: + enum class State { + Idle, + WaitingForCode, + Authenticating, + Success, + Failed, + }; + + struct JavaAccountInfo { + std::string username; + std::string uuid; + std::string tokenFile; + bool isOffline = false; + std::string authProvider; + }; + + struct AuthSlot { + std::shared_ptr auth; + + newauth::ElybyTokens elybyTokens; + newauth::JavaSession session; + std::atomic accountIndex{-1}; + mutable std::mutex mutex; + std::atomic generation{0}; + std::atomic state{State::Idle}; + mutable std::condition_variable cv; + std::string lastError; + + bool hasSession() const { return !session.uuid.empty(); } + + AuthSlot() : auth(std::make_shared()) {} + }; + + using DeviceCodeCb = newauth::DeviceCodeCallback; + using JavaCompleteCb = std::function; + + static newauthManager& Get(); + + newauthManager(const newauthManager&) = delete; + newauthManager& operator=(const newauthManager&) = delete; + + bool LoadJavaAccountIndex(); + bool SaveJavaAccountIndex() const; + + std::vector GetJavaAccounts() const; + + const AuthSlot& GetSlot(int slot) const; + + bool SetAccountForSlot(int slot, int accountIndex); + + void ClearSlot(int slot); + + newauth::JavaSession GetSlotSession(int slot) const; + + bool IsSlotLoggedIn(int slot) const; + + bool IsTokenExpiringSoon(int slot) const; + + void RefreshSlot(int slot); + + bool WaitForSlotReady(int slot, int timeoutMs = 15000) const; + + bool IsAccountInUseByOtherSlot(int slot, int accountIndex) const; + + int GetActiveJavaAccountIndex() const; + bool SetActiveJavaAccount(int index) { return SetAccountForSlot(0, index); } + newauth::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); } + + void BeginAddJavaAccount(DeviceCodeCb onDeviceCode, JavaCompleteCb onComplete, + int timeoutSeconds = 300); + + using ElybyCompleteCb = std::function; + using Elyby2FACb = std::function; + void BeginAddElybyAccount(const std::string& username, const std::string& password, + ElybyCompleteCb onComplete, Elyby2FACb on2FA = nullptr); + + int AddOfflineJavaAccount(const std::string& username); + + bool RemoveJavaAccount(int index); + + void TryRestoreActiveJavaAccount(); + + std::string GetJavaDeviceCode() const; + std::string GetJavaDirectUri() const; + std::string GetLastError() const; + + static constexpr const char* kJavaAccountsFile = "java_accounts.json"; + +private: + newauthManager(); + ~newauthManager(); + + std::string AllocTokenFile(const std::string& uuid) const; + + static newauth::JavaSession SynthesizeOfflineSession(const JavaAccountInfo& acct); + + std::shared_ptr ResetSlotAuth(AuthSlot& s); + + void RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile, + bool failOpen, bool alwaysSaveIndex); + + AuthSlot m_slots[XUSER_MAX_COUNT]; + + mutable std::mutex m_accountsMutex; + std::vector m_javaAccounts; + + mutable std::mutex m_deviceCodeMutex; + std::string m_javaDeviceCode; + std::string m_javaDirectUri; + + bool m_javaRestoreAttempted = false; +}; diff --git a/newauth/newauth.vcxproj b/newauth/newauth.vcxproj new file mode 100644 index 0000000000..f274d0ba7a --- /dev/null +++ b/newauth/newauth.vcxproj @@ -0,0 +1,156 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + Win32Proj + newauth + 10.0 + + + + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + Unicode + + + StaticLibrary + true + v143 + Unicode + + + StaticLibrary + false + v143 + Unicode + + + + + + $(SolutionDir)bin\$(Configuration)\$(Platform)\ + $(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\ + + + $(SolutionDir)bin\$(Configuration)\$(Platform)\ + $(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\ + + + $(SolutionDir)bin\$(Configuration)\$(Platform)\ + $(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\ + + + $(SolutionDir)bin\$(Configuration)\$(Platform)\ + $(SolutionDir)obj\$(ProjectName)\$(Configuration)\$(Platform)\ + + + + + Level3 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + stdcpp17 + $(ProjectDir)include;%(AdditionalIncludeDirectories) + Disabled + MultiThreadedDebug + + + $(OutDir)newauth.lib + + + + + + Level3 + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + stdcpp17 + $(ProjectDir)include;%(AdditionalIncludeDirectories) + MaxSpeed + true + true + MultiThreaded + + + $(OutDir)newauth.lib + + + + + + Level3 + true + WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions) + stdcpp17 + $(ProjectDir)include;%(AdditionalIncludeDirectories) + Disabled + MultiThreadedDebug + + + $(OutDir)newauth.lib + + + + + + Level3 + true + WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions) + stdcpp17 + $(ProjectDir)include;%(AdditionalIncludeDirectories) + MaxSpeed + true + true + MultiThreaded + + + $(OutDir)newauth.lib + + + + + + + + + + + + + + + + + + + + + diff --git a/newauth/src/newauthCrypto.cpp b/newauth/src/newauthCrypto.cpp new file mode 100644 index 0000000000..9746792869 --- /dev/null +++ b/newauth/src/newauthCrypto.cpp @@ -0,0 +1,347 @@ +#include "newauthCrypto.h" + +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "bcrypt.lib") + +#include +#include +#include +#include +#include + +namespace newauth { + +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); + } +} + +std::vector BuildSubjectPublicKeyInfoDER( + int bitSize, + const std::vector& x, + const std::vector& y) +{ + static const uint8_t OID_EC_PUBLIC_KEY[] = { 0x2a,0x86,0x48,0xce,0x3d,0x02,0x01 }; + static const uint8_t OID_P256[] = { 0x2a,0x86,0x48,0xce,0x3d,0x03,0x01,0x07 }; + 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; + + size_t algIdInnerLen = 2 + sizeof(OID_EC_PUBLIC_KEY) + 2 + curveOidLen; + + size_t bitStringContent = 1 + 1 + coordSize + coordSize; + size_t bitStringLen = bitStringContent; + + size_t outerContent = (2 + algIdInnerLen) + (2 + bitStringLen); + + std::vector 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)); + } + }; + + der.push_back(0x30); + appendLen(outerContent); + + 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); + + der.push_back(0x03); + appendLen(bitStringLen + 1); + der.push_back(0x00); + der.push_back(0x04); + der.insert(der.end(), x.begin(), x.end()); + der.insert(der.end(), y.begin(), y.end()); + + return der; +} + +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"); + + ULONG privBlobLen = 0; + ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPRIVATE_BLOB, nullptr, 0, &privBlobLen, 0), "ExportPriv(len)"); + std::vector privBlob(privBlobLen); + ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPRIVATE_BLOB, privBlob.data(), privBlobLen, &privBlobLen, 0), "ExportPriv"); + + ULONG pubBlobLen = 0; + ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPUBLIC_BLOB, nullptr, 0, &pubBlobLen, 0), "ExportPub(len)"); + std::vector pubBlob(pubBlobLen); + ThrowIfFailed(BCryptExportKey(hKey, nullptr, BCRYPT_ECCPUBLIC_BLOB, pubBlob.data(), pubBlobLen, &pubBlobLen, 0), "ExportPub"); + + BCryptDestroyKey(hKey); + BCryptCloseAlgorithmProvider(hAlg, 0); + + const BCRYPT_ECCKEY_BLOB* hdr = reinterpret_cast(pubBlob.data()); + ULONG cbKey = hdr->cbKey; + + std::vector x(pubBlob.begin() + 8, pubBlob.begin() + 8 + cbKey); + std::vector 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 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"); + + ThrowIfFailed(BCryptImportKeyPair(hAlg, nullptr, BCRYPT_ECCPRIVATE_BLOB, + &hKey, + const_cast(kp.privateBlob.data()), + (ULONG)kp.privateBlob.size(), 0), "Sign/ImportKey"); + + 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(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; + + ULONG sigLen = 0; + ThrowIfFailed(BCryptSignHash(hKey, nullptr, digest, sizeof(digest), nullptr, 0, &sigLen, 0), "Sign/GetSigLen"); + std::vector 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'); + } + 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& 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& 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 + "\"}"; +} + +static void AppendBE32(std::vector& 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& 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) +{ + using namespace std::chrono; + int64_t epochSec = duration_cast( + system_clock::now().time_since_epoch()).count(); + uint64_t winTs = (uint64_t)(epochSec + 11644473600LL) * 10000000ULL; + + std::vector content; + content.reserve(512); + + AppendBE32(content, 1); + content.push_back(0x00); + AppendBE64(content, winTs); + 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); + + std::vector sig = SignSHA256P256(p256, content.data(), content.size()); + + std::vector 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)) { + for (auto& b : bytes) b = (uint8_t)(rand() & 0xFF); + } + 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 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 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 ComputeMD5(const void* data, size_t len) { + std::vector 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; +} + +} diff --git a/newauth/src/newauthCrypto.h b/newauth/src/newauthCrypto.h new file mode 100644 index 0000000000..6d169fa80f --- /dev/null +++ b/newauth/src/newauthCrypto.h @@ -0,0 +1,50 @@ +#pragma once +#include +#include +#include + +namespace newauth { + +struct ECKeyPair { + int bitSize; + std::vector privateBlob; + std::vector publicBlob; + std::vector x; + std::vector y; + std::vector publicKeyDER; +}; + +ECKeyPair GenerateP256KeyPair(); +ECKeyPair GenerateP384KeyPair(); + +std::vector SignSHA256P256(const ECKeyPair& kp, + const uint8_t* data, + size_t len); + +std::string Base64Encode(const uint8_t* data, size_t len); +std::string Base64Encode(const std::vector& data); + +std::string Base64UrlEncode(const uint8_t* data, size_t len); +std::string Base64UrlEncode(const std::vector& data); + +std::string BuildProofKeyJson(const ECKeyPair& p256); + +std::string BuildXblSignatureHeader(const ECKeyPair& p256, + const std::string& method, + const std::string& urlPath, + const std::string& body, + const std::string& authHdr = ""); + +std::string GenerateUUID(); + +std::vector Base64Decode(const std::string& b64); +std::string Base64DecodeStr(const std::string& b64); + +std::vector ComputeMD5(const void* data, size_t len); + +std::vector BuildSubjectPublicKeyInfoDER( + int bitSize, + const std::vector& x, + const std::vector& y); + +} diff --git a/newauth/src/newauthDb.cpp b/newauth/src/newauthDb.cpp new file mode 100644 index 0000000000..e7f6c53430 --- /dev/null +++ b/newauth/src/newauthDb.cpp @@ -0,0 +1,314 @@ +#define _CRT_SECURE_NO_WARNINGS +#include "newauthDb.h" +#include "newauthInternal.h" +#include "../include/newauth.h" +#include "sqlite3.h" +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +static void DbLog(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 +} +#define DB_LOG(msg, ...) DbLog("[authdb] " msg "\n", ##__VA_ARGS__) + +namespace newauth { + +AuthDb& AuthDb::Get() { + static AuthDb instance; + return instance; +} + +AuthDb::AuthDb() = default; + +AuthDb::~AuthDb() { + Close(); +} + +bool AuthDb::Open(const char* dbPath) { + std::lock_guard lock(m_mutex); + if (m_db) return true; + + int rc = sqlite3_open(dbPath, &m_db); + if (rc != SQLITE_OK) { + DB_LOG("sqlite3_open('%s') failed: %s", dbPath, sqlite3_errmsg(m_db)); + sqlite3_close(m_db); + m_db = nullptr; + return false; + } + + sqlite3_exec(m_db, "PRAGMA journal_mode=WAL;", nullptr, nullptr, nullptr); + + if (!EnsureTables()) { + DB_LOG("EnsureTables failed"); + sqlite3_close(m_db); + m_db = nullptr; + return false; + } + + DB_LOG("opened '%s'", dbPath); + return true; +} + +void AuthDb::Close() { + std::lock_guard lock(m_mutex); + if (m_db) { + sqlite3_close(m_db); + m_db = nullptr; + } +} + +bool AuthDb::EnsureTables() { + const char* sql = + "CREATE TABLE IF NOT EXISTS accounts (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " username TEXT NOT NULL," + " uuid TEXT NOT NULL," + " token_key TEXT," + " is_offline INTEGER DEFAULT 0," + " auth_provider TEXT DEFAULT 'mojang'" + ");" + "CREATE TABLE IF NOT EXISTS config (" + " key TEXT PRIMARY KEY," + " value TEXT" + ");" + "CREATE TABLE IF NOT EXISTS tokens (" + " key TEXT PRIMARY KEY," + " blob TEXT NOT NULL" + ");"; + + char* err = nullptr; + int rc = sqlite3_exec(m_db, sql, nullptr, nullptr, &err); + if (rc != SQLITE_OK) { + DB_LOG("EnsureTables error: %s", err ? err : "unknown"); + sqlite3_free(err); + return false; + } + return true; +} + +bool AuthDb::LoadAccounts(int& activeIndex, std::vector& accounts) { + std::lock_guard lock(m_mutex); + if (!m_db) return false; + + accounts.clear(); + activeIndex = -1; + + { + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(m_db, + "SELECT value FROM config WHERE key='activeIndex'", -1, &stmt, nullptr); + if (rc == SQLITE_OK && sqlite3_step(stmt) == SQLITE_ROW) { + const char* v = (const char*)sqlite3_column_text(stmt, 0); + if (v) activeIndex = atoi(v); + } + sqlite3_finalize(stmt); + } + + { + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(m_db, + "SELECT id, username, uuid, token_key, is_offline, auth_provider " + "FROM accounts ORDER BY id", -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + DB_LOG("LoadAccounts prepare failed: %s", sqlite3_errmsg(m_db)); + return false; + } + while (sqlite3_step(stmt) == SQLITE_ROW) { + AccountRow row; + row.id = sqlite3_column_int(stmt, 0); + row.username = (const char*)sqlite3_column_text(stmt, 1); + row.uuid = (const char*)sqlite3_column_text(stmt, 2); + const char* tk = (const char*)sqlite3_column_text(stmt, 3); + row.tokenKey = tk ? tk : ""; + row.isOffline = sqlite3_column_int(stmt, 4) != 0; + const char* ap = (const char*)sqlite3_column_text(stmt, 5); + row.authProvider = ap ? ap : "mojang"; + accounts.push_back(std::move(row)); + } + sqlite3_finalize(stmt); + } + + DB_LOG("LoadAccounts: %d accounts, activeIndex=%d", (int)accounts.size(), activeIndex); + return true; +} + +bool AuthDb::SaveAccounts(int activeIndex, const std::vector& accounts) { + std::lock_guard lock(m_mutex); + if (!m_db) return false; + + char* err = nullptr; + sqlite3_exec(m_db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr); + + sqlite3_exec(m_db, "DELETE FROM accounts;", nullptr, nullptr, nullptr); + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(m_db, + "INSERT INTO accounts (username, uuid, token_key, is_offline, auth_provider) " + "VALUES (?, ?, ?, ?, ?)", -1, &stmt, nullptr); + if (rc != SQLITE_OK) { + DB_LOG("SaveAccounts prepare failed: %s", sqlite3_errmsg(m_db)); + sqlite3_exec(m_db, "ROLLBACK;", nullptr, nullptr, nullptr); + return false; + } + + for (auto& a : accounts) { + sqlite3_reset(stmt); + sqlite3_bind_text(stmt, 1, a.username.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, a.uuid.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 3, a.tokenKey.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_int(stmt, 4, a.isOffline ? 1 : 0); + sqlite3_bind_text(stmt, 5, a.authProvider.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + + rc = sqlite3_prepare_v2(m_db, + "INSERT OR REPLACE INTO config (key, value) VALUES ('activeIndex', ?)", + -1, &stmt, nullptr); + if (rc == SQLITE_OK) { + char buf[32]; + snprintf(buf, sizeof(buf), "%d", activeIndex); + sqlite3_bind_text(stmt, 1, buf, -1, SQLITE_TRANSIENT); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } + + sqlite3_exec(m_db, "COMMIT;", nullptr, nullptr, &err); + if (err) { DB_LOG("SaveAccounts commit: %s", err); sqlite3_free(err); return false; } + + DB_LOG("SaveAccounts: saved %d accounts, activeIndex=%d", (int)accounts.size(), activeIndex); + return true; +} + +bool AuthDb::SaveTokenBlob(const std::string& key, const std::string& blob) { + std::lock_guard lock(m_mutex); + if (!m_db) return false; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(m_db, + "INSERT OR REPLACE INTO tokens (key, blob) VALUES (?, ?)", + -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(stmt, 2, blob.c_str(), -1, SQLITE_TRANSIENT); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE; +} + +std::string AuthDb::LoadTokenBlob(const std::string& key) { + std::lock_guard lock(m_mutex); + if (!m_db) return ""; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(m_db, + "SELECT blob FROM tokens WHERE key=?", -1, &stmt, nullptr); + if (rc != SQLITE_OK) return ""; + + sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); + std::string result; + if (sqlite3_step(stmt) == SQLITE_ROW) { + const char* v = (const char*)sqlite3_column_text(stmt, 0); + if (v) result = v; + } + sqlite3_finalize(stmt); + return result; +} + +bool AuthDb::DeleteTokenBlob(const std::string& key) { + std::lock_guard lock(m_mutex); + if (!m_db) return false; + + sqlite3_stmt* stmt = nullptr; + int rc = sqlite3_prepare_v2(m_db, + "DELETE FROM tokens WHERE key=?", -1, &stmt, nullptr); + if (rc != SQLITE_OK) return false; + + sqlite3_bind_text(stmt, 1, key.c_str(), -1, SQLITE_TRANSIENT); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + return rc == SQLITE_DONE; +} + +bool AuthDb::MigrateFromJson(const char* jsonPath) { + std::ifstream f(jsonPath); + if (!f) return false; + + DB_LOG("MigrateFromJson: found '%s', importing...", jsonPath); + + std::string content((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + f.close(); + + int activeIndex = (int)JsonGetInt(content, "activeIndex"); + + std::vector accounts; + std::vector 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) { + AccountRow row; + row.username = JsonGetString(obj, "username"); + row.uuid = JsonGetString(obj, "uuid"); + row.tokenKey = JsonGetString(obj, "tokenFile"); + row.isOffline = (JsonRawValue(obj, "isOffline") == "true"); + row.authProvider = JsonGetString(obj, "authProvider"); + if (row.authProvider.empty()) + row.authProvider = row.isOffline ? "offline" : "mojang"; + if (!row.tokenKey.empty() || row.isOffline) + accounts.push_back(std::move(row)); + } + + if (!SaveAccounts(activeIndex, accounts)) { + DB_LOG("MigrateFromJson: SaveAccounts failed"); + return false; + } + + for (auto& acct : accounts) { + if (acct.tokenKey.empty() || acct.isOffline) continue; + std::ifstream tf(acct.tokenKey); + if (!tf) continue; + std::string tokenContent((std::istreambuf_iterator(tf)), + std::istreambuf_iterator()); + tf.close(); + if (!tokenContent.empty()) { + SaveTokenBlob(acct.tokenKey, tokenContent); + DB_LOG("MigrateFromJson: imported token '%s'", acct.tokenKey.c_str()); + } + } + + std::string backupPath = std::string(jsonPath) + ".migrated"; + std::rename(jsonPath, backupPath.c_str()); + DB_LOG("MigrateFromJson: done, renamed '%s' -> '%s'", jsonPath, backupPath.c_str()); + + return true; +} + +} diff --git a/newauth/src/newauthDb.h b/newauth/src/newauthDb.h new file mode 100644 index 0000000000..7bbb775b7b --- /dev/null +++ b/newauth/src/newauthDb.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +struct sqlite3; + +namespace newauth { + +struct AccountRow { + int id = 0; + std::string username; + std::string uuid; + std::string tokenKey; + bool isOffline = false; + std::string authProvider; +}; + +class AuthDb { +public: + static AuthDb& Get(); + + AuthDb(const AuthDb&) = delete; + AuthDb& operator=(const AuthDb&) = delete; + + bool Open(const char* dbPath = "newauth.db"); + void Close(); + + bool LoadAccounts(int& activeIndex, std::vector& accounts); + bool SaveAccounts(int activeIndex, const std::vector& accounts); + + bool SaveTokenBlob(const std::string& key, const std::string& blob); + std::string LoadTokenBlob(const std::string& key); + bool DeleteTokenBlob(const std::string& key); + + bool MigrateFromJson(const char* jsonPath = "java_accounts.json"); + +private: + AuthDb(); + ~AuthDb(); + + bool EnsureTables(); + + sqlite3* m_db = nullptr; + std::mutex m_mutex; +}; + +} diff --git a/newauth/src/newauthElyby.cpp b/newauth/src/newauthElyby.cpp new file mode 100644 index 0000000000..373d44418a --- /dev/null +++ b/newauth/src/newauthElyby.cpp @@ -0,0 +1,160 @@ +#include "../include/newauth.h" +#include "newauthCrypto.h" +#include "newauthHttp.h" +#include "newauthInternal.h" + +#include +#include + +namespace newauth { + +using namespace newauth; + +bool ElybyLogin(const std::string& username, const std::string& password, + ElybyTokens& outTokens, std::string& error) +{ + 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"); + + 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; + } + + 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; +} + +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; +} + +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"); + return (resp.statusCode == 204 || resp.statusCode == 200); + } catch (const std::exception& e) { + error = std::string("ElybyValidate network error: ") + e.what(); + } + return false; +} + +bool ElybyLoadTokens(const std::string& path, ElybyTokens& out) +{ + std::ifstream f(path); + if (!f) return false; + std::string content((std::istreambuf_iterator(f)), + std::istreambuf_iterator()); + 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::ofstream f(path); + if (!f) return false; + f << ElybySerializeTokens(tokens); + return f.good(); +} + +std::string ElybySerializeTokens(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 << "}"; + return o.str(); +} + +bool ElybyDeserializeTokens(const std::string& json, ElybyTokens& out) +{ + if (json.empty()) return false; + out.accessToken = JsonGetString(json, "accessToken"); + out.clientToken = JsonGetString(json, "clientToken"); + out.uuid = JsonGetString(json, "uuid"); + out.username = JsonGetString(json, "username"); + return !out.accessToken.empty(); +} + +} diff --git a/newauth/src/newauthHttp.cpp b/newauth/src/newauthHttp.cpp new file mode 100644 index 0000000000..2dce1de796 --- /dev/null +++ b/newauth/src/newauthHttp.cpp @@ -0,0 +1,152 @@ +#include "newauthHttp.h" +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "winhttp.lib") + +#include +#include + +namespace newauth { + +struct ParsedUrl { + std::wstring scheme; + std::wstring host; + INTERNET_PORT port; + std::wstring path; +}; + +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; +} + +struct WinHttpHandle { + HINTERNET h = nullptr; + WinHttpHandle() = default; + explicit WinHttpHandle(HINTERNET handle) : h(handle) {} + ~WinHttpHandle() { if (h) WinHttpCloseHandle(h); } + WinHttpHandle(const WinHttpHandle&) = delete; + WinHttpHandle& operator=(const WinHttpHandle&) = delete; + WinHttpHandle(WinHttpHandle&& o) noexcept : h(o.h) { o.h = nullptr; } + operator HINTERNET() const { return h; } + explicit operator bool() const { return h != nullptr; } +}; + +static HttpResponse DoRequest(const std::wstring& method, + const ParsedUrl& parsed, + const std::string& body, + const std::string& contentType, + const std::map& headers, + const std::string& urlForErrors) +{ + bool isHttps = (parsed.scheme == L"https"); + + WinHttpHandle session(WinHttpOpen( + L"newauth/1.0", + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, + WINHTTP_NO_PROXY_BYPASS, + 0)); + if (!session) + throw std::runtime_error("WinHttpOpen failed"); + + WinHttpSetTimeouts(session, 10000, 10000, 10000, 15000); + + WinHttpHandle connect(WinHttpConnect(session, parsed.host.c_str(), parsed.port, 0)); + if (!connect) + throw std::runtime_error("WinHttpConnect failed for host: " + WideToUtf8(parsed.host)); + + DWORD flags = isHttps ? WINHTTP_FLAG_SECURE : 0; + WinHttpHandle request(WinHttpOpenRequest( + connect, method.c_str(), parsed.path.c_str(), + nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, flags)); + if (!request) + throw std::runtime_error("WinHttpOpenRequest failed"); + + if (!contentType.empty()) { + std::wstring ct = L"Content-Type: " + Utf8ToWide(contentType); + WinHttpAddRequestHeaders(request, ct.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD); + } + + for (auto& kv : headers) { + std::wstring h = Utf8ToWide(kv.first) + L": " + Utf8ToWide(kv.second); + WinHttpAddRequestHeaders(request, h.c_str(), (DWORD)-1, WINHTTP_ADDREQ_FLAG_ADD); + } + + BOOL sent = WinHttpSendRequest( + request, + WINHTTP_NO_ADDITIONAL_HEADERS, 0, + body.empty() ? WINHTTP_NO_REQUEST_DATA : const_cast(body.c_str()), + (DWORD)body.size(), (DWORD)body.size(), 0); + + if (!sent || !WinHttpReceiveResponse(request, nullptr)) + throw std::runtime_error("request failed for: " + urlForErrors); + + DWORD statusCode = 0, statusLen = sizeof(statusCode); + WinHttpQueryHeaders(request, + 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(request, &bytesAvailable) && bytesAvailable > 0) { + std::string chunk(bytesAvailable, '\0'); + DWORD bytesRead = 0; + WinHttpReadData(request, chunk.data(), bytesAvailable, &bytesRead); + responseBody.append(chunk.data(), bytesRead); + } + + return { (int)statusCode, std::move(responseBody) }; +} + +HttpResponse HttpPost(const std::string& url, + const std::string& body, + const std::string& contentType, + const std::map& headers) +{ + return DoRequest(L"POST", ParseUrl(url), body, contentType, headers, url); +} + +HttpResponse HttpGet(const std::string& url, + const std::map& headers) +{ + return DoRequest(L"GET", ParseUrl(url), "", "", headers, url); +} + +} diff --git a/newauth/src/newauthHttp.h b/newauth/src/newauthHttp.h new file mode 100644 index 0000000000..707b9e7162 --- /dev/null +++ b/newauth/src/newauthHttp.h @@ -0,0 +1,20 @@ +#pragma once +#include +#include + +namespace newauth { + +struct HttpResponse { + int statusCode; + std::string body; +}; + +HttpResponse HttpPost(const std::string& url, + const std::string& body, + const std::string& contentType, + const std::map& headers = {}); + +HttpResponse HttpGet(const std::string& url, + const std::map& headers = {}); + +} diff --git a/newauth/src/newauthInternal.h b/newauth/src/newauthInternal.h new file mode 100644 index 0000000000..02301c8c8b --- /dev/null +++ b/newauth/src/newauthInternal.h @@ -0,0 +1,205 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace newauth { + +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 JsonArrayObjects(const std::string& json) { + std::vector 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(system_clock::now().time_since_epoch()).count(); +} + +inline std::string BuildFormBody(const std::map& 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; +} + +} diff --git a/newauth/src/newauthJava.cpp b/newauth/src/newauthJava.cpp new file mode 100644 index 0000000000..c6a75c4f13 --- /dev/null +++ b/newauth/src/newauthJava.cpp @@ -0,0 +1,423 @@ +#include "../include/newauth.h" +#include "newauthCrypto.h" +#include "newauthHttp.h" +#include "newauthInternal.h" +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace newauth { + +using namespace newauth; + +struct JavaAuthManager::Impl { + + struct MsaState { + std::string accessToken, refreshToken; + int64_t expireMs = 0; + bool IsExpired() const { return NowMs() >= expireMs - 60000; } + } msa; + + ECKeyPair deviceKeyP256; + std::string deviceId; + + 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 loggedIn{false}; + + std::atomic cancelRequested{false}; + std::mutex cancelMutex; + std::condition_variable cancelCv; + + bool SleepOrCancel(int64_t ms) { + std::unique_lock lock(cancelMutex); + return cancelCv.wait_for(lock, std::chrono::milliseconds(ms), [this] { + return cancelRequested.load(std::memory_order_relaxed); + }); + } + + Impl() { + deviceKeyP256 = GenerateP256KeyPair(); + deviceId = GenerateUUID(); + } + + 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}, + {"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; + } + + 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; + } + + 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; + } + + 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; + } + + // god this protocol is convoluted + 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 + "," + "\"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; + } + + 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; + } + + 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; + } + + bool FetchProfile(JavaSession& out, std::string& error) { + auto resp = HttpGet( + "https://api.minecraftservices.com/minecraft/profile", + {{"Authorization", mcToken.AuthHeader()}}); + + if (resp.statusCode == 404) { + 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.accessToken; + out.expireMs = mcToken.expireMs; + if (out.uuid.empty() || out.username.empty()) { + error = "Minecraft profile response missing uuid or username"; + return false; + } + return true; + } + + 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) { + 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; + } + + std::string Serialise() const { + std::ostringstream o; + o << "{\n" + << "\"msaAccessToken\":" << newauth::JsonStr(msa.accessToken) << ",\n" + << "\"msaRefreshToken\":" << newauth::JsonStr(msa.refreshToken) << ",\n" + << "\"msaExpireMs\":" << msa.expireMs << ",\n" + << "\"deviceId\":" << newauth::JsonStr(deviceId) << ",\n" + << "\"deviceKeyPriv\":" << newauth::JsonStr(Base64Encode(deviceKeyP256.privateBlob)) << ",\n" + << "\"deviceKeyPub\":" << newauth::JsonStr(Base64Encode(deviceKeyP256.publicBlob)) << "\n" + << "}"; + return o.str(); + } + + static ECKeyPair RebuildP256(const std::vector& priv, + const std::vector& pub) { + if (pub.size() < sizeof(BCRYPT_ECCKEY_BLOB)) return {}; + const BCRYPT_ECCKEY_BLOB* hdr = + reinterpret_cast(pub.data()); + ULONG cbKey = hdr->cbKey; + if (pub.size() < 8 + (size_t)cbKey * 2) return {}; + std::vector x(pub.begin()+8, pub.begin()+8+cbKey); + std::vector 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()) {} +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(f)), + std::istreambuf_iterator()); + return m_impl->Deserialise(content); +} + +std::string JavaAuthManager::SerializeTokens() const { + return m_impl->Serialise(); +} + +bool JavaAuthManager::DeserializeTokens(const std::string& json) { + return m_impl->Deserialise(json); +} + +} diff --git a/newauth/src/newauthManager.cpp b/newauth/src/newauthManager.cpp new file mode 100644 index 0000000000..f6eb223785 --- /dev/null +++ b/newauth/src/newauthManager.cpp @@ -0,0 +1,956 @@ +#define _CRT_SECURE_NO_WARNINGS +#include "../include/newauthManager.h" +#include "newauthInternal.h" +#include "newauthDb.h" +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#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 + FILE* f = _fsopen("newauth_debug.log", "a", _SH_DENYWR); + if (f) { fputs(buf, f); fclose(f); } +} +#define AUTH_LOG(msg, ...) AuthLogImpl("[newauth] " msg "\n", ##__VA_ARGS__) + +using namespace newauth; + +newauthManager& newauthManager::Get() { + static newauthManager instance; + return instance; +} + +newauthManager::newauthManager() = default; + +newauthManager::~newauthManager() { + 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 newauthManager::ResetSlotAuth(AuthSlot& s) { + if (s.auth) + s.auth->RequestCancel(); + + auto fresh = std::make_shared(); + s.auth = fresh; + return fresh; +} + +newauth::JavaSession newauthManager::SynthesizeOfflineSession(const JavaAccountInfo& acct) { + newauth::JavaSession s; + s.username = acct.username; + s.uuid = acct.uuid; + s.accessToken = ""; + s.expireMs = 0; + return s; +} + +bool newauthManager::LoadJavaAccountIndex() { + auto& db = AuthDb::Get(); + if (!db.Open()) { + AUTH_LOG("LoadJavaAccountIndex: DB open failed"); + return false; + } + + db.MigrateFromJson(kJavaAccountsFile); + + int activeIndex = -1; + std::vector rows; + if (!db.LoadAccounts(activeIndex, rows)) { + AUTH_LOG("LoadJavaAccountIndex: DB LoadAccounts failed"); + return false; + } + + std::lock_guard lock(m_accountsMutex); + m_javaAccounts.clear(); + + for (auto& row : rows) { + JavaAccountInfo info; + info.username = row.username; + info.uuid = row.uuid; + info.tokenFile = row.tokenKey; + info.isOffline = row.isOffline; + info.authProvider = row.authProvider; + if (info.authProvider.empty()) + info.authProvider = info.isOffline ? "offline" : "mojang"; + if (!info.tokenFile.empty() || info.isOffline) + m_javaAccounts.push_back(std::move(info)); + } + + 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 newauthManager::SaveJavaAccountIndex() const { + std::lock_guard lock(m_accountsMutex); + + std::vector rows; + for (auto& a : m_javaAccounts) { + AccountRow row; + row.username = a.username; + row.uuid = a.uuid; + row.tokenKey = a.tokenFile; + row.isOffline = a.isOffline; + row.authProvider = a.authProvider.empty() ? "mojang" : a.authProvider; + rows.push_back(std::move(row)); + } + + int activeIndex = m_slots[0].accountIndex.load(); + if (!AuthDb::Get().SaveAccounts(activeIndex, rows)) { + AUTH_LOG("SaveJavaAccountIndex: DB SaveAccounts failed"); + return false; + } + + AUTH_LOG("SaveJavaAccountIndex: saved %d accounts, active=%d", + (int)m_javaAccounts.size(), activeIndex); + return true; +} + +std::vector newauthManager::GetJavaAccounts() const { + std::lock_guard lock(m_accountsMutex); + return m_javaAccounts; +} + +std::string newauthManager::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 newauthManager::AuthSlot& newauthManager::GetSlot(int slot) const { + if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0; + return m_slots[slot]; +} + +newauth::JavaSession newauthManager::GetSlotSession(int slot) const { + if (slot < 0 || slot >= XUSER_MAX_COUNT) slot = 0; + auto& s = m_slots[slot]; + + { + std::lock_guard 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 lock(s.mutex); + return s.session; +} + +bool newauthManager::IsSlotLoggedIn(int slot) const { + if (slot < 0 || slot >= XUSER_MAX_COUNT) return false; + auto& s = m_slots[slot]; + + { + std::lock_guard 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 slock(s.mutex); + return !s.elybyTokens.accessToken.empty(); + } + } + } + + return s.auth && s.auth->IsLoggedIn(); +} + +bool newauthManager::IsTokenExpiringSoon(int slot) const { + if (slot < 0 || slot >= XUSER_MAX_COUNT) return false; + auto& s = m_slots[slot]; + std::lock_guard lock(s.mutex); + if (s.session.expireMs <= 0) return false; + return NowMs() > s.session.expireMs - 60000; +} + +bool newauthManager::IsAccountInUseByOtherSlot(int slot, int accountIndex) const { + std::lock_guard 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 newauthManager::RunElybyRefresh(int slot, uint32_t gen, const std::string& tokenFile, + bool failOpen, bool alwaysSaveIndex) { + auto& s = m_slots[slot]; + std::string error; + newauth::ElybyTokens tokens; + { + std::lock_guard lock(s.mutex); + tokens = s.elybyTokens; + } + bool ok = newauth::ElybyRefresh(tokens, error); + if (s.generation != gen) return; + + { + std::lock_guard lock(s.mutex); + if (ok) { + s.elybyTokens = tokens; + s.session.username = tokens.username; + s.session.uuid = newauth::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()) + AuthDb::Get().SaveTokenBlob(tokenFile, newauth::ElybySerializeTokens(tokens)); + { + std::lock_guard 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 = newauth::DashUuid(tokens.uuid); + } + } + } + if (ok || alwaysSaveIndex) + SaveJavaAccountIndex(); +} + +void newauthManager::RefreshSlot(int slot) { + if (slot < 0 || slot >= XUSER_MAX_COUNT) return; + auto& s = m_slots[slot]; + + std::string provider; + std::string elyTokenFile; + { + std::lock_guard 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 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; + } + + uint32_t gen = ++s.generation; + + auto authCopy = s.auth; + + { + std::lock_guard lock(s.mutex); + s.state = State::Authenticating; + s.cv.notify_all(); + } + + std::thread([this, slot, gen, authCopy]() { + auto& s = m_slots[slot]; + std::string error; + newauth::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"; + } + + 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 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(); + } + + if (ok) { + std::lock_guard 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()); + + if (ok && !tokenFile.empty()) { + AuthDb::Get().SaveTokenBlob(tokenFile, authCopy->SerializeTokens()); + SaveJavaAccountIndex(); + } + }).detach(); +} + +bool newauthManager::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 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; + } + + uint32_t gen = ++s.generation; + + if (isOffline) { + newauth::JavaSession offlineSession; + std::string offlineName; + { + std::lock_guard alock(m_accountsMutex); + offlineSession = SynthesizeOfflineSession(m_javaAccounts[accountIndex]); + offlineName = m_javaAccounts[accountIndex].username; + } + { + std::lock_guard 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) { + { + std::lock_guard lock(s.mutex); + s.accountIndex = accountIndex; + } + + AUTH_LOG("SetAccountForSlot(%d): switching to elyby account %d, tokenFile='%s'", + slot, accountIndex, tokenFile.c_str()); + + newauth::ElybyTokens elyTokens; + { + std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile); + if (blob.empty() || !newauth::ElybyDeserializeTokens(blob, elyTokens)) { + AUTH_LOG("SetAccountForSlot(%d): ElybyLoadTokens from DB failed", slot); + std::lock_guard lock(s.mutex); + s.state = State::Failed; + s.cv.notify_all(); + return false; + } + } + + { + std::lock_guard lock(s.mutex); + s.elybyTokens = elyTokens; + s.session.username = elyTokens.username; + s.session.uuid = newauth::DashUuid(elyTokens.uuid); + s.session.accessToken = elyTokens.accessToken; + s.session.expireMs = 0; + s.state = State::Authenticating; + s.cv.notify_all(); + } + + uint32_t elyGen = ++s.generation; + std::thread([this, slot, elyGen, tokenFile]() { + RunElybyRefresh(slot, elyGen, tokenFile, /*failOpen=*/true, /*alwaysSaveIndex=*/true); + }).detach(); + } else { + { + std::lock_guard 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); + + { + std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile); + if (blob.empty() || !freshAuth->DeserializeTokens(blob)) { + AUTH_LOG("SetAccountForSlot(%d): LoadTokens from DB failed", slot); + std::lock_guard lock(s.mutex); + s.state = State::Failed; + s.cv.notify_all(); + return false; + } + } + + RefreshSlot(slot); + } + + SaveJavaAccountIndex(); + return true; +} + +void newauthManager::ClearSlot(int slot) { + if (slot < 0 || slot >= XUSER_MAX_COUNT) return; + auto& s = m_slots[slot]; + + ++s.generation; + + ResetSlotAuth(s); + + std::lock_guard lock(s.mutex); + s.session = {}; + s.accountIndex = -1; + s.state = State::Idle; + s.lastError.clear(); + s.cv.notify_all(); +} + +bool newauthManager::WaitForSlotReady(int slot, int timeoutMs) const { + if (slot < 0 || slot >= XUSER_MAX_COUNT) return false; + auto& s = m_slots[slot]; + std::unique_lock 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 newauthManager::GetActiveJavaAccountIndex() const { + return m_slots[0].accountIndex; +} + +newauth::JavaSession newauthManager::GetJavaSession() const { + return GetSlotSession(0); +} + +bool newauthManager::IsJavaLoggedIn() const { + return IsSlotLoggedIn(0); +} + +int newauthManager::AddOfflineJavaAccount(const std::string& username) { + if (username.empty()) return -1; + + std::string uuid = newauth::GenerateOfflineUuid(username); + if (uuid.empty()) return -1; + + std::lock_guard alock(m_accountsMutex); + + for (int i = 0; i < (int)m_javaAccounts.size(); ++i) { + if (m_javaAccounts[i].uuid == uuid) { + m_javaAccounts[i].username = username; + + { + std::lock_guard 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; + + { + std::lock_guard 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 newauthManager::BeginAddJavaAccount(DeviceCodeCb onDeviceCode, + JavaCompleteCb onComplete, + int timeoutSeconds) +{ + auto& s = m_slots[0]; + + uint32_t gen = ++s.generation; + + { + std::lock_guard lock(m_deviceCodeMutex); + m_javaDeviceCode.clear(); + m_javaDirectUri.clear(); + } + + { + std::lock_guard 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]; + + newauth::JavaSession session; + std::string error; + + bool ok = freshAuth->Login( + [&](const newauth::DeviceCodeInfo& dc) { + if (s.generation != gen) return; + + { + std::lock_guard lock(s.mutex); + s.state = State::WaitingForCode; + s.cv.notify_all(); + } + { + std::lock_guard lock(m_deviceCodeMutex); + m_javaDeviceCode = dc.userCode; + m_javaDirectUri = dc.directUri; + } + if (onDeviceCode) onDeviceCode(dc); + { + std::lock_guard lock(s.mutex); + s.state = State::Authenticating; + s.cv.notify_all(); + } + }, + session, error, timeoutSeconds); + + { + std::lock_guard 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 alock(m_accountsMutex); + + 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()); + } + } + + if (!tokenFile.empty()) + AuthDb::Get().SaveTokenBlob(tokenFile, freshAuth->SerializeTokens()); + + { + std::lock_guard lock(s.mutex); + s.session = session; + s.lastError.clear(); + s.state = State::Success; + s.cv.notify_all(); + } + + SaveJavaAccountIndex(); + } else { + { + std::lock_guard lock(s.mutex); + s.lastError = error; + s.state = State::Failed; + s.cv.notify_all(); + } + + std::string reloadFile; + { + std::lock_guard alock(m_accountsMutex); + if (s.accountIndex >= 0 && s.accountIndex < (int)m_javaAccounts.size()) + reloadFile = m_javaAccounts[s.accountIndex].tokenFile; + } + if (!reloadFile.empty()) { + std::string blob = AuthDb::Get().LoadTokenBlob(reloadFile); + if (!blob.empty()) freshAuth->DeserializeTokens(blob); + } + } + + 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 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 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 newauthManager::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 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]; + newauth::ElybyTokens tokens; + std::string error; + + bool ok = newauth::ElybyLogin(username, password, tokens, error); + + if (!ok && error == "elyby_2fa_required") { + { + std::lock_guard lock(s.mutex); + s.state = State::WaitingForCode; + s.cv.notify_all(); + } + if (on2FA) on2FA(); + if (onComplete) onComplete(false, {}, "elyby_2fa_required"); + return; + } + + { + std::lock_guard lock(s.mutex); + if (s.generation != gen) { + if (onComplete) onComplete(false, {}, "cancelled"); + return; + } + } + + if (ok) { + std::string dashedUuid = newauth::DashUuid(tokens.uuid); + std::string tokenFile; + { + std::lock_guard 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()) + AuthDb::Get().SaveTokenBlob(tokenFile, newauth::ElybySerializeTokens(tokens)); + + newauth::JavaSession session; + session.username = tokens.username; + session.uuid = dashedUuid; + session.accessToken = tokens.accessToken; + session.expireMs = 0; + + { + std::lock_guard 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 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 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 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 newauthManager::RemoveJavaAccount(int index) { + std::lock_guard 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); + + for (int i = 0; i < XUSER_MAX_COUNT; ++i) { + auto& s = m_slots[i]; + if (s.accountIndex == index) { + if (m_javaAccounts.empty()) { + s.accountIndex = -1; + std::lock_guard lock(s.mutex); + s.session = {}; + s.state = State::Idle; + s.cv.notify_all(); + } else { + s.accountIndex = 0; + } + } else if (s.accountIndex > index) { + --s.accountIndex; + } + } + + return true; +} + +void newauthManager::TryRestoreActiveJavaAccount() { + AUTH_LOG("TryRestoreActiveJavaAccount called (already attempted=%d)", + (int)m_javaRestoreAttempted); + + if (m_javaRestoreAttempted) return; + m_javaRestoreAttempted = true; + + if (!LoadJavaAccountIndex()) { + auto& s = m_slots[0]; + std::string legacyBlob; + { + std::ifstream lf("java_auth.json"); + if (lf) legacyBlob.assign((std::istreambuf_iterator(lf)), + std::istreambuf_iterator()); + } + if (!legacyBlob.empty() && s.auth->DeserializeTokens(legacyBlob)) { + AUTH_LOG("TryRestoreActiveJavaAccount: migrating legacy java_auth.json"); + std::lock_guard alock(m_accountsMutex); + JavaAccountInfo info; + info.username = ""; + info.uuid = ""; + info.tokenFile = "legacy_migrated"; + AuthDb::Get().SaveTokenBlob(info.tokenFile, s.auth->SerializeTokens()); + m_javaAccounts.push_back(std::move(info)); + s.accountIndex = 0; + SaveJavaAccountIndex(); + 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 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()); + newauth::ElybyTokens elyTokens; + { + std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile); + if (blob.empty() || !newauth::ElybyDeserializeTokens(blob, elyTokens)) { + AUTH_LOG("TryRestoreActiveJavaAccount: ElybyLoadTokens from DB failed for '%s'", tokenFile.c_str()); + std::lock_guard lock(s.mutex); + s.state = State::Failed; + s.lastError = "Failed to load ely.by tokens: " + tokenFile; + s.cv.notify_all(); + return; + } + } + { + std::lock_guard lock(s.mutex); + s.elybyTokens = elyTokens; + s.session.username = elyTokens.username; + s.session.uuid = newauth::DashUuid(elyTokens.uuid); + s.session.accessToken = elyTokens.accessToken; + s.session.expireMs = 0; + s.state = State::Success; + s.cv.notify_all(); + } + RefreshSlot(0); + return; + } + + if (isOffline) { + AUTH_LOG("TryRestoreActiveJavaAccount: offline account %d, no refresh needed", activeIdx); + newauth::JavaSession offlineSession; + { + std::lock_guard alock(m_accountsMutex); + offlineSession = SynthesizeOfflineSession(m_javaAccounts[activeIdx]); + } + { + std::lock_guard 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); + + { + std::string blob = AuthDb::Get().LoadTokenBlob(tokenFile); + if (blob.empty() || !s.auth->DeserializeTokens(blob)) { + AUTH_LOG("TryRestoreActiveJavaAccount: LoadTokens from DB failed for '%s'", tokenFile.c_str()); + std::lock_guard lock(s.mutex); + s.state = State::Failed; + s.lastError = "Failed to load token: " + tokenFile; + s.cv.notify_all(); + return; + } + } + + RefreshSlot(0); +} + +std::string newauthManager::GetJavaDeviceCode() const { + std::lock_guard lock(m_deviceCodeMutex); + return m_javaDeviceCode; +} + +std::string newauthManager::GetJavaDirectUri() const { + std::lock_guard lock(m_deviceCodeMutex); + return m_javaDirectUri; +} + +std::string newauthManager::GetLastError() const { + auto& s = m_slots[0]; + std::lock_guard lock(s.mutex); + return s.lastError; +} diff --git a/newauth/src/newauthSession.cpp b/newauth/src/newauthSession.cpp new file mode 100644 index 0000000000..79eeccb3fc --- /dev/null +++ b/newauth/src/newauthSession.cpp @@ -0,0 +1,410 @@ +#include "../include/newauth.h" +#include "newauthCrypto.h" +#include "newauthHttp.h" +#include "newauthInternal.h" +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include +#pragma comment(lib, "bcrypt.lib") +#pragma comment(lib, "windowscodecs.lib") +#pragma comment(lib, "ole32.lib") + +namespace newauth { + +using namespace newauth; + +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; + 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); + + hash[6] = (hash[6] & 0x0F) | 0x30; + hash[8] = (hash[8] & 0x3F) | 0x80; + + 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; +} + +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); +} + +static void ParseTextureProperties(const std::string& body, std::string& skinUrl, std::string& capeUrl) { + size_t propsPos = body.find("\"properties\""); + if (propsPos == std::string::npos) return; + + size_t texNamePos = body.find("\"textures\"", propsPos); + if (texNamePos == std::string::npos) return; + + std::string b64Value = JsonGetString(body.substr(texNamePos), "value"); + if (b64Value.empty()) return; + + std::string decoded = Base64DecodeStr(b64Value); + if (decoded.empty()) return; + + 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); +} + +static std::vector CropSkinTo64x32(const std::vector& pngData) { + 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; + + 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; } + + #define WIC_CHECK(expr) do { hr = (expr); if (FAILED(hr)) { cleanup(); return pngData; } } while(0) + + 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)); + + if (width != 64 || height != 64) { cleanup(); return pngData; } + + WIC_CHECK(factory->CreateFormatConverter(&converter)); + WIC_CHECK(converter->Initialize(frame, GUID_WICPixelFormat32bppBGRA, + WICBitmapDitherTypeNone, nullptr, 0.0, WICBitmapPaletteTypeCustom)); + + const UINT cropW = 64, cropH = 32; + const UINT stride = cropW * 4; + std::vector pixels(stride * cropH); + + WICRect cropRect = { 0, 0, (INT)cropW, (INT)cropH }; + WIC_CHECK(converter->CopyPixels(&cropRect, stride, (UINT)pixels.size(), pixels.data())); + + 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()); + + STATSTG stat = {}; + WIC_CHECK(memStream->Stat(&stat, STATFLAG_NONAME)); + ULONG pngSize = (ULONG)stat.cbSize.QuadPart; + + #undef WIC_CHECK + + std::vector result(pngSize); + LARGE_INTEGER zero = {}; + memStream->Seek(zero, STREAM_SEEK_SET, nullptr); + memStream->Read(result.data(), pngSize, nullptr); + + cleanup(); + return result; + + } catch (...) { + return pngData; + } +} + +bool ValidateSkinPng(const uint8_t* data, size_t size) { + static const uint8_t kPngMagic[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + + if (data == nullptr || size < 24) + return false; + if (size > kMaxSkinBytes) + return false; + if (memcmp(data, kPngMagic, 8) != 0) + return false; + + 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]; + + if (width != 64) + return false; + if (height != 32 && height != 64) + return false; + + return true; +} + +static std::vector FetchSkinPngImpl(const std::string& url, bool cropToLCE, 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(resp.body.begin(), resp.body.end()); + if (!ValidateSkinPng(rawPng.data(), rawPng.size())) { + error = "FetchSkinPng: invalid PNG data (" + std::to_string(rawPng.size()) + " bytes)"; + return {}; + } + return cropToLCE ? CropSkinTo64x32(rawPng) : rawPng; + } + error = "FetchSkinPng HTTP " + std::to_string(resp.statusCode); + } catch (const std::exception& e) { + error = std::string("FetchSkinPng exception: ") + e.what(); + } + return {}; +} + +std::vector FetchSkinPng(const std::string& url, std::string& error) { + return FetchSkinPngImpl(url, /*cropToLCE=*/true, error); +} + +std::vector FetchSkinPngRaw(const std::string& url, std::string& error) { + return FetchSkinPngImpl(url, /*cropToLCE=*/false, error); +} + +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); +} + +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); +} + +}