mirror of
https://github.com/smartcmd/MinecraftConsoles.git
synced 2026-08-20 09:57:09 +00:00
The old Windows64 port had no real player identity — it used hardcoded fake XUIDs, so anyone could impersonate anyone. This replaces that with proper auth supporting Mojang, Ely.by, and offline accounts. MCAuth library (new, MCAuth/): Mojang auth via MSA device code flow (XBL, SISU, MC services), Ely.by via Yggdrasil with 2FA, offline UUID generation matching Java Edition (MD5 v3 from "OfflinePlayer:<name>"). Multi-account manager with background token refresh, per-slot sessions, and on-disk token persistence. Server-side session verification via Mojang/Ely.by hasJoined API. Skin fetching and PNG validation from texture servers. Network protocol (version bumped to 80): Three new packets (AuthScheme, AuthResponse, AuthResult) implement a server-driven auth handshake before login completes. Player identity migrated from 64-bit XUID to 128-bit GameUUID backed by two uint64 fields (hi/lo). readPlayerUID/writePlayerUID now serialize 16 bytes on the wire. Old and new clients cannot connect to each other — version mismatch is rejected at PreLogin. Save migration: Map data mappings auto-migrate from old format: the old 64-bit XUID is placed in hi, lo is set to 0 as a sentinel. On first access by the real player, the sentinel entry is upgraded in-place to the full 128-bit UUID. Format detection is by file size (2080, 2112, or 4160 bytes). Player .dat filenames inside saveData.ms change from decimal XUID to dashed UUID — old saves need manual entry renaming in the archive. UI: NativeUIRenderer: immediate-mode drawing system (quads, text, 9-slice panels, scrollbars, focus lists) for rendering auth screens without Flash/Scaleform. UIScene_MSAuth handles device code display, Ely.by credential input with 2FA, per-account skin head preview, and multi-account add/remove/switch. Server: online-mode and auth-provider (mojang/elyby) in server.properties. Whitelist and ban checks validate against the server-verified UUID. Incompatible auth scheme logs which provider the server expects vs what the client is using. Also fixes a pre-existing exploit where any client could send a DebugOptionsPacket to grant themselves CraftAnything and other debug privileges on any server — now requires OP status server-side.
1713 lines
64 KiB
C++
1713 lines
64 KiB
C++
#include "stdafx.h"
|
|
#include "UI.h"
|
|
#include "UIScene_MSAuth.h"
|
|
#include "NativeUIRenderer.h"
|
|
#include "../../../MCAuth/include/MCAuthManager.h"
|
|
#include "../../../MCAuth/include/MCAuth.h"
|
|
#include <thread>
|
|
#include <fstream>
|
|
#include <sys/stat.h>
|
|
|
|
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<int*>(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<SkinEntry>();
|
|
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<const char*>(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<AuthFlags>();
|
|
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<std::shared_ptr<UIScene_MSAuth::PendingKeyboardResult>*>(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<wchar_t*>(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<std::string, std::shared_ptr<UIScene_MSAuth::SkinEntry>>;
|
|
|
|
|
|
// 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<MCAuthManager::JavaAccountInfo>& 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<int>(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<PendingKeyboardResult>();
|
|
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<PendingKeyboardResult>(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<PendingKeyboardResult>();
|
|
UIKeyboardInitData kbData;
|
|
kbData.title = L"2FA Code";
|
|
kbData.defaultText = L"";
|
|
kbData.maxChars = 10;
|
|
kbData.callback = &MSAuthKeyboardCallback;
|
|
kbData.lpParam = new std::shared_ptr<PendingKeyboardResult>(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<PendingKeyboardResult>();
|
|
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<PendingKeyboardResult>(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
|