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