#include "stdafx.h" #include "UI.h" #include "UIScene_MSAuth.h" #include "NativeUIRenderer.h" #include "../../../MCAuth/include/MCAuthManager.h" #include "../../../MCAuth/include/MCAuth.h" #include #include #include using namespace MSAuthUI; #ifdef _WINDOWS64 #include "../../Windows64/KeyboardMouseInput.h" #include "../../Minecraft.h" extern KeyboardMouseInput g_KBMInput; #endif // Auto-close ~3 seconds after auth result (~30 fps). static constexpr int kAutoCloseTicks = 90; // Sentinel value: skin PNG downloaded to disk, awaiting texture load on main thread. static constexpr int TEXTURE_PENDING_LOAD = -3; // Max account rows visible at once (scrollable beyond this). static constexpr int kMaxVisible = 7; // Panel dimensions (1280x720 virtual canvas) static constexpr float kPX = 290.0f, kPY = 85.0f, kPW = 700.0f, kPH = 550.0f; // Account row layout — taller rows with space for skin head 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 layout inside rows static constexpr float kHeadSize = 36.0f; static constexpr float kHeadPad = 8.0f; static constexpr float kTextAfterHead = kHeadSize + kHeadPad * 2; // List area geometry (starts right after title + divider) static constexpr float kListTop = kPY + 54.0f; static constexpr float kListH = kMaxVisible * (kRowH + kRowGap) - kRowGap; // Button bar static constexpr float kBtnY = kPY + kPH - 54.0f; static constexpr float kBtnH = 40.0f; // Constructor UIScene_MSAuth::UIScene_MSAuth(int iPad, void* initData, UILayer* parentLayer) : UIScene(iPad, parentLayer) { if (initData) m_targetSlot = *reinterpret_cast(initData); else m_targetSlot = 0; initialiseMovie(); SwitchToAccountList(); } UIScene_MSAuth::~UIScene_MSAuth() { // Invalidate any pending keyboard result so the callback (which may fire // after this scene is destroyed) writes into the shared buffer harmlessly. if (m_pendingKBResult) m_pendingKBResult->valid.store(false, std::memory_order_release); } // Skin texture loading (async download + disk cache) static const char* kSkinCacheDir = "skins"; void UIScene_MSAuth::EnsureSkinLoaded(const std::string& uuid) { if (uuid.empty()) return; // Already in cache? auto it = m_skinCache.find(uuid); if (it != m_skinCache.end()) { auto& entry = *it->second; // If downloaded to disk but not yet loaded as a texture, load it now (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; } // Create cache entry and start background download auto entry = std::make_shared(); entry->textureId.store(-1); // -1 = downloading entry->filePath = std::string(kSkinCacheDir) + "/" + uuid + ".png"; m_skinCache[uuid] = entry; // Check if already cached on disk 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; } // Determine auth provider for this UUID std::string skinProvider = "mojang"; for (auto& acct : m_accounts) { if (acct.uuid == uuid || MCAuth::UndashUuid(acct.uuid) == uuid) { skinProvider = acct.authProvider; break; } } // Background download std::string uuidCopy = uuid; std::thread([entry, uuidCopy, skinProvider]() { try { std::string error; std::string skinUrl; if (skinProvider == "elyby") skinUrl = MCAuth::ElybyFetchProfileSkinUrl(uuidCopy, error); else skinUrl = MCAuth::FetchProfileSkinUrl(uuidCopy, error); if (skinUrl.empty()) { entry->textureId.store(-1); return; } auto pngData = MCAuth::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(); // Signal main thread to load the texture (can't call D3D from here) entry->textureId.store(TEXTURE_PENDING_LOAD); } catch (...) { // Network failure (no internet) — mark skin as unavailable, don't crash entry->textureId.store(-1); } }).detach(); } // View switching 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; // Create a NEW AuthFlags instance so the old thread's onComplete callback // (which captured the old shared_ptr) cannot affect this flow. m_authFlags = std::make_shared(); m_closeCountdown = 0; StartAddAccount(); } static int MSAuthKeyboardCallback(LPVOID lpParam, bool bRes) { // lpParam is a heap-allocated shared_ptr; take ownership and delete it. 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)); // Pass through all printable ASCII — ely.by fields need more than alnum+underscore. // Field-specific 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; MCAuthManager::Get().BeginAddElybyAccount( m_elybyUsername, m_elybyPassword, [flags](bool ok, const MCAuth::JavaSession&, const std::string& error) { if (error == "elyby_2fa_required") return; // handled by on2FA callback 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; // Show spinner while authenticating m_closeCountdown = 0; } void UIScene_MSAuth::SubmitElyby2FA() { if (m_elyby2FACode.empty()) return; m_textInputActive = false; // Retry with password:totp std::string combinedPassword = m_elybyPassword + ":" + m_elyby2FACode; auto flags = m_authFlags; MCAuthManager::Get().BeginAddElybyAccount( m_elybyUsername, combinedPassword, [flags](bool ok, const MCAuth::JavaSession&, const std::string&) { flags->success.store(ok, std::memory_order_release); flags->done.store(true, std::memory_order_release); } ); m_view = eView_DeviceCode; // Show spinner m_closeCountdown = 0; } void UIScene_MSAuth::ConfirmOfflineAccount() { if (m_offlineUsername.empty()) return; int idx = MCAuthManager::Get().AddOfflineJavaAccount(m_offlineUsername); if (idx >= 0) MCAuthManager::Get().SaveJavaAccountIndex(); SwitchToAccountList(); } void UIScene_MSAuth::StartAddAccount() { // Capture by value so flags outlive the scene auto flags = m_authFlags; MCAuthManager::Get().BeginAddJavaAccount( nullptr, [flags](bool ok, const MCAuth::JavaSession&, const std::string&) { flags->success.store(ok, std::memory_order_release); flags->done.store(true, std::memory_order_release); } ); } // tick() void UIScene_MSAuth::tick() { UIScene::tick(); ++m_spinnerTick; if (m_inputGuardTicks > 0) --m_inputGuardTicks; m_accounts = MCAuthManager::Get().GetJavaAccounts(); m_activeIdx = MCAuthManager::Get().GetSlot(m_targetSlot).accountIndex; // Trigger skin downloads for online accounts (non-blocking) for (auto& acct : m_accounts) { if (!acct.isOffline && !acct.uuid.empty()) EnsureSkinLoaded(acct.uuid); } // Pick up result from virtual keyboard callback (shared_ptr pattern) 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 { // Offline username: filter to alnum + underscore, max 16 chars 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) { ++m_offlineCursorBlink; // Deactivate text input if focus moved away from the text box if (m_textInputActive && !m_focus.IsActive(eBtn_OfflineTextBox)) m_textInputActive = false; #ifdef _WINDOWS64 // Only consume keyboard input when the text box is active 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 { // Drain the char buffer so stale keys don't fire when text box is re-selected wchar_t ch; while (g_KBMInput.ConsumeChar(ch)) {} } #endif } // Check for ely.by 2FA request (flag lives in shared AuthFlags for async safety) 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; // Determine which text box is active based on elybyActiveField 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; // allow all printable chars for password/2FA 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 = MCAuthManager::Get().GetJavaDirectUri(); } // ---- Build focus list ---- int prevFocus = m_focus.GetFocused(); m_focus.Clear(); if (m_view == eView_AccountList) { if (m_pendingRemoveIdx >= 0) { // Confirm dialog — only Yes/No buttons, No is first (default) 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 (default selected), 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 — centered quad 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 — selectable input field 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); // Bottom 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(); } // Helper: draw a step number circle 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); } // Helper: draw a Steve head placeholder (or skin head when available) static void DrawHeadPlaceholder(float x, float y, float size) { // Outer border — dark stone grey NativeUI::DrawRoundedRect(x, y, size, size, 3.0f, 0xFF2A2A2Au); // Inner — slightly lighter (Steve skin tone placeholder) NativeUI::DrawRoundedRect(x + 2, y + 2, size - 4, size - 4, 2.0f, 0xFF4A3728u); // Eyes 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); } // Helper: draw a player head from a skin texture (face + hat overlay) static void DrawSkinHead(float x, float y, float size, int skinTexId) { // Face layer: pixels (8,8)-(16,16) in 64x64 skin 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 layer: pixels (40,8)-(48,16) in 64x64 skin 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 type alias for static render functions using SkinCacheMap = std::unordered_map>; // Render — Account List View // Returns 1-based player number if account at accountIndex is assigned to another slot, 0 if free. static int GetSlotUsingAccount(int selfSlot, int accountIndex) { for (int i = 0; i < XUSER_MAX_COUNT; ++i) { if (i == selfSlot) continue; if (MCAuthManager::Get().GetSlot(i).accountIndex == accountIndex) return i + 1; // 1-based player number } 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 area if (accounts.empty()) { // Empty state — centered message 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; // Recessed list background 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 for scrollable list 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 background — MC style 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/hover border highlight if (rowFocused || rowHovered) { uint32_t borderColor = rowFocused ? 0xAAFFFFFFu : 0x60FFFFFFu; NativeUI::DrawRoundedBorder(kRowLeft, y, kAccountTextW, kRowH, 4.0f, 1.0f, borderColor); } // Active indicator — green left bar if (isActive) NativeUI::DrawRoundedRect(kRowLeft, y, 4.0f, kRowH, 2.0f, 0xFF55DD55u); // Player head — skin texture for online, Steve placeholder for offline 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 { // Loading placeholder — dark square with spinner NativeUI::DrawRoundedRect(headX, headY, kHeadSize, kHeadSize, 3.0f, 0xFF222222u); } } // Username — always after head area 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); // Subtitle line: type badge + UUID float subY = textY + 21.0f; // Type badge 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 after the pill 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 badge or "Player N" badge on the right 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 button 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 arrow indicators — side by side, below the list on the right { 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 are 32x22 native, render 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 above buttons NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); // Bottom buttons — centered quad 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 confirmation dialog ---- if (pendingRemoveIdx >= 0 && pendingRemoveIdx < (int)accounts.size()) { // Dim everything behind the dialog NativeUI::DrawRect(kPX, kPY, kPW, kPH, 0xA0000000u); // Dialog panel — same 9-slice as the main panel (authentic MC look) 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); // Account name being removed 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); // Buttons: [Cancel] [Remove] — Cancel is default (left, first in focus list) 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)); } } // Render — Device Code View static void RenderDeviceCode(const NativeUI::NineSlice& panel, int spinnerTick, int closeCountdown, bool authSuccess, NativeUI::FocusList& focus, MCAuthManager& 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(); // --- Success state --- if (state == MCAuthManager::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); MCAuth::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); } // Progress bar for auto-close countdown 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); } // --- Failure state --- else if (state == MCAuthManager::State::Failed) { // 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 code / Authenticating --- else if (state == MCAuthManager::State::WaitingForCode || state == MCAuthManager::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: Visit URL float stepY1 = kPY + 66.0f; DrawStepCircle(kPX + 50.0f, stepY1 + 10.0f, 1, state == MCAuthManager::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: Enter code 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 box — prominent centered display 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 indicator float statusY = kPY + 290.0f; NativeUI::DrawSpinner(600.0f, statusY + 6.0f, 8.0f, spinnerTick); const wchar_t* status = (state == MCAuthManager::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 / initial state NativeUI::DrawSpinner(640.0f, kPY + 220.0f, 20.0f, spinnerTick); } // Divider above button NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); // Cancel button — centered NativeUI::DrawButton(kPX + (kPW - 200.0f) * 0.5f, kBtnY, 200.0f, kBtnH, L"Cancel", focus.ShowFocus(eBtn_Back), focus.IsHovered(eBtn_Back)); } // Render — Offline Username Input View 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); // Description 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); // Username label NativeUI::DrawShadowText(640.0f, kPY + 134.0f, L"Username", 0xFFDDDDDDu, 14.0f, NativeUI::ALIGN_CENTER_X); // Text input box — wider, centered 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); // Draw text box with focus highlight 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); // Display the typed username std::wstring display(username.begin(), username.end()); // Only show blinking cursor when text input is active 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); // Character counter { 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 above buttons NativeUI::DrawDivider(kPX + 20.0f, kBtnY - 12.0f, kPW - 40.0f, 0x30FFFFFFu); // Bottom buttons — centered pair 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)); } // Render — Ely.by Login Input View 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 field 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 field 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 above buttons 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)); } // Render — Ely.by 2FA Dialog View 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 field 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 above buttons 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)); } // render() 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 the entire screen 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, MCAuthManager::Get()); } NativeUI::EndFrame(); } // Input 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; } // Account list scroll: intercept UP/DOWN when focused on edge rows // to scroll the list instead of jumping to the 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 scroll for account list 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 confirmation 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 = MCAuthManager::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(); MCAuthManager::Get().RemoveJavaAccount(idx); MCAuthManager::Get().SaveJavaAccountIndex(); accounts = MCAuthManager::Get().GetJavaAccounts(); int newActive = MCAuthManager::Get().GetActiveJavaAccountIndex(); if (!accounts.empty() && newActive >= 0) MCAuthManager::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) { 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 (MCAuthManager::Get().IsAccountInUseByOtherSlot(m_targetSlot, idx)) { app.DebugPrintf("[MSAuth] Account %d already in use by another player slot\n", idx); } else { MCAuthManager::Get().SetAccountForSlot(m_targetSlot, idx); #ifdef _WINDOWS64 if (m_targetSlot > 0) { 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 (Windows64 only) #ifdef _WINDOWS64 bool UIScene_MSAuth::handleMouseClick(F32 /*x*/, F32 /*y*/) { return m_focus.IsMouseConsumed(); } #endif