using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using Firebase; using Firebase.Auth; using Firebase.Firestore; using System; using System.Linq; using static BulletHellTemplate.PlayerSave; namespace BulletHellTemplate { public class FirebaseManager : MonoBehaviour { [Header("Component responsible for loading and saving user data")] public FirebaseAuth auth; public FirebaseFirestore firestore; public static FirebaseManager Singleton; [Header("Daily Quest Reset Configuration")] public int resetHour = 0; private void Awake() { if (Singleton == null) { Singleton = this; } else { Destroy(gameObject); return; } DontDestroyOnLoad(gameObject); } public void Start() { StartInitializeFirebase(); } public void StartInitializeFirebase() { InitializeFirebase(); } private async void InitializeFirebase() { try { var dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync(); if (dependencyStatus == DependencyStatus.Available) { auth = FirebaseAuth.DefaultInstance; firestore = FirebaseFirestore.DefaultInstance; FirebaseAuthManager.Singleton.InitializeAuthBackend(auth, firestore); FirebaseSave.Singleton.InitializeFirebase(auth.CurrentUser, firestore); Debug.Log("Firebase initialized successfully."); } else { Debug.LogError("Could not resolve all Firebase dependencies: " + dependencyStatus); } } catch (Exception e) { Debug.LogError("Failed to initialize Firebase: " + e.Message); } } /// /// Loads and synchronizes all required player data from Firestore in a single method. /// If the player's document does not exist, it will create default data once. /// This approach removes repeated checks and reduces extra snapshot calls. /// /// The authenticated user's ID. /// A result message describing the outcome. public async Task LoadAndSyncPlayerData(string userId) { // Single checks at the beginning if (firestore == null) { return "Failed to initialize Firestore."; } if (!MonetizationManager.Singleton.IsInitialized()) { return "MonetizationManager is not initialized."; } if (auth == null || auth.CurrentUser == null) { return "No user is currently logged in."; } PlayerPrefs.DeleteAll(); PlayerPrefs.Save(); try { // 1) Check or create the main document DocumentReference docRef = firestore.Collection("Players").Document(userId); DocumentSnapshot snapshot = await docRef.GetSnapshotAsync(); if (!snapshot.Exists) { await CreateDefaultPlayerData(docRef); snapshot = await docRef.GetSnapshotAsync(); if (!snapshot.Exists) { return "Failed to create default player data."; } } // 2) Parse minimal fields in the main document Dictionary playerData = snapshot.ToDictionary(); if (playerData.TryGetValue("PlayerName", out object pName) && pName != null) { PlayerSave.SetPlayerName(pName.ToString()); } if (playerData.TryGetValue("PlayerIcon", out object pIcon) && pIcon != null) { PlayerSave.SetPlayerIcon(pIcon.ToString()); } if (playerData.TryGetValue("PlayerFrame", out object pFrame) && pFrame != null) { PlayerSave.SetPlayerFrame(pFrame.ToString()); } if (playerData.TryGetValue("AccountLevel", out object aLevelObj) && aLevelObj != null) { // Convert the object to int properly, handling Firestore's usual 'long' storage int levelValue = Convert.ToInt32(aLevelObj); PlayerSave.SetAccountLevel(levelValue); } else { // If the field doesn't exist or is null, set a default PlayerSave.SetAccountLevel(1); } if (playerData.TryGetValue("AccountCurrentExp", out object aExpObj) && aExpObj != null) { int expValue = Convert.ToInt32(aExpObj); PlayerSave.SetAccountCurrentExp(expValue); } else { PlayerSave.SetAccountCurrentExp(0); } // 3) Run all other sub-loads in parallel to speed up the process List loadTasks = new List { LoadAndSyncCurrencies(userId), LoadPurchasedItemsAsync(userId), LoadSelectedCharacter(userId), LoadPlayerCharacterFavouriteAsync(userId), LoadAndSyncAllCharacterUpgradesAsync(userId), LoadUnlockedMapsAsync(userId), ResetDailyQuestsIfNeeded(userId), LoadQuestsAsync(userId), LoadUsedCouponsAsync(userId), CheckForSeasonEndAsync(userId), LoadBattlePassProgressAsync(), LoadClaimedRewardsFromFirebase(userId), InitializeInventoryAsync(), LoadAndSyncAllCharacterBasicInfoAsync(), LoadCharacterSlotsAsync(), LoadAndSyncAllCharacterUnlockedSkinsAsync(), }; await Task.WhenAll(loadTasks); await LoadRewardsDataAsync(); BackendManager.Singleton.SetInitialized(); return "Player data loaded successfully."; } catch (Exception ex) { Debug.LogError($"Failed to load and synchronize player data: {ex.Message}"); return $"Failed to load player data: {ex.Message}"; } } /// /// Creates a default player document if none exists. /// private async Task CreateDefaultPlayerData(DocumentReference docRef) { try { string playerName = "GUEST-" + UnityEngine.Random.Range(100000, 999999); string defaultIconId = GameInstance.Singleton.iconItems[0].iconId; string defaultFrameId = GameInstance.Singleton.frameItems[0].frameId; Dictionary defaultData = new Dictionary { { "PlayerName", playerName }, { "PlayerIcon", defaultIconId }, { "PlayerFrame", defaultFrameId } }; await docRef.SetAsync(defaultData); Debug.Log($"Created default player data for user document: {docRef.Id}"); } catch (Exception ex) { Debug.LogError($"Failed to create default player data: {ex.Message}"); throw; } } /// /// Loads and synchronizes the player's currencies (parallel-ready, no repeated checks). /// private async Task LoadAndSyncCurrencies(string userId) { try { CollectionReference currenciesCollection = firestore .Collection("Players") .Document(userId) .Collection("Currencies"); foreach (Currency currency in MonetizationManager.Singleton.currencies) { DocumentReference currencyDoc = currenciesCollection.Document(currency.coinID); DocumentSnapshot snapshot = await currencyDoc.GetSnapshotAsync(); if (snapshot.Exists) { int amount = snapshot.GetValue("amount"); MonetizationManager.SetCurrency(currency.coinID, amount); } else { Dictionary currencyData = new Dictionary { { "initialAmount", currency.initialAmount }, { "amount", currency.initialAmount } }; await currencyDoc.SetAsync(currencyData); MonetizationManager.SetCurrency(currency.coinID, currency.initialAmount); } } Debug.Log("Currencies loaded successfully."); } catch (Exception e) { Debug.LogError($"Failed to load currencies: {e.Message}"); } } /// /// Loads the selected character from the main document, parallel-ready. /// private async Task LoadSelectedCharacter(string userId) { try { DocumentReference docRef = firestore.Collection("Players").Document(userId); DocumentSnapshot snapshot = await docRef.GetSnapshotAsync(); if (snapshot.Exists) { int selectedCharacter = 0; if (snapshot.ContainsField("selectedCharacter")) { selectedCharacter = snapshot.GetValue("selectedCharacter"); } else { await docRef.UpdateAsync("selectedCharacter", 0); } PlayerSave.SetSelectedCharacter(selectedCharacter); } else { Dictionary data = new Dictionary { { "selectedCharacter", 0 } }; await docRef.SetAsync(data); PlayerSave.SetSelectedCharacter(0); } } catch (Exception e) { Debug.LogError($"Failed to load selected character: {e.Message}"); } } // /// Loads purchased items without using 'dynamic'. Each subdocument is handled with a specific typed method. /// private async Task LoadPurchasedItemsAsync(string userId) { try { CollectionReference docRef = firestore .Collection("Players") .Document(userId) .Collection("PurchasedItems"); // Default lists var defaultCharacterList = new PurchasedCharacterList(new List()); var defaultIconList = new PurchasedIconList(new List()); var defaultFrameList = new PurchasedFrameList(new List()); var defaultShopItemList = new PurchasedShopItemList(new List()); // Call each "ensure + load" method separately Task taskCharacters = EnsureCharactersDocExistsAndLoad(docRef.Document("Characters"), "Characters", defaultCharacterList); Task taskIcons = EnsureIconsDocExistsAndLoad(docRef.Document("Icons"), "Icons", defaultIconList); Task taskFrames = EnsureFramesDocExistsAndLoad(docRef.Document("Frames"), "Frames", defaultFrameList); Task taskShopItems = EnsureShopItemsDocExistsAndLoad(docRef.Document("ShopItems"), "ShopItems", defaultShopItemList); // Wait for all to complete await Task.WhenAll(taskCharacters, taskIcons, taskFrames, taskShopItems); // Retrieve the final typed results PurchasedCharacterList finalCharacters = taskCharacters.Result; PurchasedIconList finalIcons = taskIcons.Result; PurchasedFrameList finalFrames = taskFrames.Result; PurchasedShopItemList finalShopItems = taskShopItems.Result; PurchasedInventoryItemList finalItems = new PurchasedInventoryItemList(new List()); // Merge results into MonetizationManager MonetizationManager.Singleton.UpdatePurchasedItems( finalCharacters.purchasedCharacters, finalIcons.purchasedIcons, finalFrames.purchasedFrames, finalShopItems.purchasedShopItems, finalItems.purchasedInventoryItems ); } catch (Exception ex) { Debug.LogError("Error loading purchased items: " + ex.Message); } } /// /// Ensures the 'Characters' subdocument exists, creates it if not, and returns a PurchasedCharacterList. /// private async Task EnsureCharactersDocExistsAndLoad( DocumentReference docRef, string fieldName, PurchasedCharacterList defaultValue) { var snapshot = await docRef.GetSnapshotAsync(); if (!snapshot.Exists) { // Create a new document with an empty or default list await docRef.SetAsync(new Dictionary { { fieldName, defaultValue.purchasedCharacters } // use the .purchasedCharacters list }); return defaultValue; } else { // Load existing data var listData = snapshot.GetValue>(fieldName); var typed = ConvertToList(listData); return new PurchasedCharacterList(typed); } } /// /// Ensures the 'Icons' subdocument exists, creates it if not, and returns a PurchasedIconList. /// private async Task EnsureIconsDocExistsAndLoad( DocumentReference docRef, string fieldName, PurchasedIconList defaultValue) { var snapshot = await docRef.GetSnapshotAsync(); if (!snapshot.Exists) { await docRef.SetAsync(new Dictionary { { fieldName, defaultValue.purchasedIcons } }); return defaultValue; } else { var listData = snapshot.GetValue>(fieldName); var typed = ConvertToList(listData); return new PurchasedIconList(typed); } } /// /// Ensures the 'Frames' subdocument exists, creates it if not, and returns a PurchasedFrameList. /// private async Task EnsureFramesDocExistsAndLoad( DocumentReference docRef, string fieldName, PurchasedFrameList defaultValue) { var snapshot = await docRef.GetSnapshotAsync(); if (!snapshot.Exists) { await docRef.SetAsync(new Dictionary { { fieldName, defaultValue.purchasedFrames } }); return defaultValue; } else { var listData = snapshot.GetValue>(fieldName); var typed = ConvertToList(listData); return new PurchasedFrameList(typed); } } /// /// Ensures the 'ShopItems' subdocument exists, creates it if not, and returns a PurchasedShopItemList. /// private async Task EnsureShopItemsDocExistsAndLoad( DocumentReference docRef, string fieldName, PurchasedShopItemList defaultValue) { var snapshot = await docRef.GetSnapshotAsync(); if (!snapshot.Exists) { await docRef.SetAsync(new Dictionary { { fieldName, defaultValue.purchasedShopItems } }); return defaultValue; } else { var listData = snapshot.GetValue>(fieldName); var typed = ConvertToList(listData); return new PurchasedShopItemList(typed); } } /// /// Loads a single character's upgrades and stores them locally. /// private async Task LoadCharacterUpgradesAsync(string userId, int characterId) { try { DocumentReference upgradesDocRef = firestore .Collection("Players") .Document(userId) .Collection("Characters") .Document(characterId.ToString()) .Collection("Upgrades") .Document("Stats"); DocumentSnapshot snapshot = await upgradesDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary upgradesData = snapshot.ToDictionary(); foreach (var entry in upgradesData) { if (Enum.TryParse(entry.Key, out StatType statType)) { int level = Convert.ToInt32(entry.Value); PlayerSave.SaveCharacterUpgradeLevel(characterId, statType, level); } } } else { // If no upgrades, save local defaults as 0 foreach (StatType statType in Enum.GetValues(typeof(StatType))) { PlayerSave.SaveInitialCharacterUpgradeLevel(characterId, statType); } } } catch (Exception e) { Debug.LogError($"Failed to load character upgrades for {characterId}: {e.Message}"); } } /// /// Loads and saves all character upgrades in parallel for each unlocked character. /// private async Task LoadAndSyncAllCharacterUpgradesAsync(string userId) { try { List upgradeTasks = new List(); foreach (CharacterData character in GameInstance.Singleton.characterData) { if (character.CheckUnlocked || MonetizationManager.Singleton.IsCharacterPurchased(character.characterId.ToString())) { upgradeTasks.Add(LoadCharacterUpgradesAsync(userId, character.characterId)); } } await Task.WhenAll(upgradeTasks); } catch (Exception e) { Debug.LogError("Failed to load all character upgrades: " + e.Message); } } /// /// Loads the unlocked skins for a specific character from Firebase Firestore and saves them locally. /// /// The user's ID. /// The ID of the character. public async Task LoadCharacterUnlockedSkinsAsync(string userId, int characterId) { try { DocumentReference skinsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Characters") .Document(characterId.ToString()) .Collection("UnlockedSkins") .Document("Skins"); DocumentSnapshot snapshot = await skinsDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary skinsData = snapshot.ToDictionary(); if (skinsData.ContainsKey("skins")) { List unlockedSkins = new List(); // Firestore returns numeric array elements as long. if (skinsData["skins"] is IList skinsList) { foreach (var skin in skinsList) { if (skin is long l) { unlockedSkins.Add((int)l); } else if (skin is int i) { unlockedSkins.Add(i); } } } // Save unlocked skins locally using PlayerSave. PlayerSave.SaveCharacterUnlockedSkins(characterId, unlockedSkins); Debug.Log($"Unlocked skins for character {characterId} loaded and saved locally."); } else { Debug.LogWarning($"No 'skins' field found for character {characterId}."); } } else { Debug.LogWarning($"No unlocked skins document found for character {characterId}."); } } catch (Exception e) { Debug.LogError($"Failed to load unlocked skins for character {characterId}: {e.Message}"); } } /// /// Loads and synchronizes unlocked skins for all unlocked or purchased characters. /// public async Task LoadAndSyncAllCharacterUnlockedSkinsAsync() { if (auth.CurrentUser == null) { Debug.LogError("No user is logged in."); return; } try { List tasks = new List(); foreach (CharacterData character in GameInstance.Singleton.characterData) { if (character.CheckUnlocked || MonetizationManager.Singleton.IsCharacterPurchased(character.characterId.ToString())) { tasks.Add(LoadCharacterUnlockedSkinsAsync(auth.CurrentUser.UserId, character.characterId)); } } await Task.WhenAll(tasks); } catch (Exception e) { Debug.LogError("Failed to load all character unlocked skins: " + e.Message); } } /// /// Loads unlocked maps, clearing local data first. /// private async Task LoadUnlockedMapsAsync(string userId) { try { DocumentReference mapsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("UnlockedMaps"); DocumentSnapshot snapshot = await mapsDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary mapsData = snapshot.ToDictionary(); List unlockedMapIds = new List(); foreach (var entry in mapsData) { if (int.TryParse(entry.Key, out int mapId)) { unlockedMapIds.Add(mapId); } } PlayerSave.SetUnlockedMaps(unlockedMapIds); } } catch (Exception e) { Debug.LogError("Failed to load unlocked maps: " + e.Message); } } /// /// Loads quests from the "Quests" document and saves them locally. /// private async Task LoadQuestsAsync(string userId) { try { DocumentReference questsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("Quests"); DocumentSnapshot snapshot = await questsDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary questsData = snapshot.ToDictionary(); foreach (var entry in questsData) { if (entry.Key.StartsWith("Complete")) { string[] parts = entry.Key.Split(' '); if (parts.Length == 2 && int.TryParse(parts[1], out int questId)) { PlayerSave.SaveQuestCompletion(questId); } } else if (int.TryParse(entry.Key, out int questId)) { int progress = Convert.ToInt32(entry.Value); PlayerSave.SaveQuestProgress(questId, progress); } } } } catch (Exception e) { Debug.LogError("Failed to load quests: " + e.Message); } } /// /// Checks if daily quests need resetting based on server timestamp. /// private async Task ResetDailyQuestsIfNeeded(string userId) { try { DocumentReference serverTimeRef = firestore .Collection("Players") .Document(userId) .Collection("ServerTime") .Document("CurrentTime"); await serverTimeRef.SetAsync(new Dictionary { { "timestamp", FieldValue.ServerTimestamp } }); DocumentSnapshot serverTimeSnapshot = await serverTimeRef.GetSnapshotAsync(); Timestamp serverTimestamp = serverTimeSnapshot.GetValue("timestamp"); DateTime serverDateTime = serverTimestamp.ToDateTime(); DateTime resetTimeToday = serverDateTime.Date.AddHours(resetHour); await ResetDailyQuestProgressInFirebase(userId, serverDateTime, resetTimeToday); } catch (Exception e) { Debug.LogError("Failed to reset daily quests: " + e.Message); } } private async Task ResetDailyQuestProgressInFirebase(string userId, DateTime serverDateTime, DateTime resetTimeToday) { try { DocumentReference questsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("Quests"); DocumentSnapshot snapshot = await questsDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary questsData = snapshot.ToDictionary(); foreach (var entry in questsData) { if (entry.Key.StartsWith("Complete ")) { string[] parts = entry.Key.Split(' '); if (parts.Length == 2 && int.TryParse(parts[1], out int questId)) { QuestItem questItem = GameInstance.Singleton.questData.FirstOrDefault(q => q.questId == questId); if (questItem != null && questItem.questType == QuestType.Daily) { string completionTimeKey = $"Complete {questId}_Timestamp"; if (questsData.TryGetValue(completionTimeKey, out object timestampValue) && timestampValue is Timestamp completionTimestamp) { DateTime completionTime = completionTimestamp.ToDateTime(); if (completionTime < resetTimeToday) { await questsDocRef.UpdateAsync(new Dictionary { { questId.ToString(), 0 } }); await questsDocRef.UpdateAsync(new Dictionary { { "Complete " + questId, FieldValue.Delete } }); await questsDocRef.UpdateAsync(new Dictionary { { "Complete " + questId + "_Timestamp", FieldValue.Delete } }); PlayerPrefs.DeleteKey(completionTimeKey); Debug.Log($"Quest {questId} has been reset."); } } } } } } } } catch (Exception e) { Debug.LogError("Failed to reset daily quest progress: " + e.Message); } } /// /// Loads both Daily Rewards and New Player Rewards data from Firebase, /// retrieves the server time, and saves it locally in PlayerSave. /// If the documents do not exist, they are created with default values. /// public async Task LoadRewardsDataAsync() { FirebaseUser user = auth.CurrentUser; if (user == null) { Debug.LogError("Cannot load rewards data. No authenticated user found."); return; } DateTime serverDateTime = await FetchServerTimeAsync(user.UserId); if (serverDateTime == DateTime.MinValue) { serverDateTime = DateTime.Now; } DailyRewardsData dailyData = await LoadDailyRewardsFromFirebase(user.UserId, serverDateTime); NewPlayerRewardsData newPlayerData = await LoadNewPlayerRewardsFromFirebase(user.UserId); PlayerSave.SetDailyRewardsLocal(dailyData); PlayerSave.SetNewPlayerRewardsLocal(newPlayerData); DateTime resetTimeToday = serverDateTime.Date.AddHours(resetHour); DateTime nextReset = (serverDateTime >= resetTimeToday) ? resetTimeToday.AddDays(1) : resetTimeToday; PlayerSave.SetNextDailyReset(nextReset); Debug.Log($"Daily & New Player rewards loaded. Next reset: {nextReset}"); } /// /// Fetches the current server time from Firestore and returns it as a DateTime. /// private async Task FetchServerTimeAsync(string userId) { try { DocumentReference serverTimeRef = firestore .Collection("Players") .Document(userId) .Collection("ServerTime") .Document("CurrentTime"); await serverTimeRef.SetAsync(new Dictionary { { "timestamp", FieldValue.ServerTimestamp } }); DocumentSnapshot snapshot = await serverTimeRef.GetSnapshotAsync(); Timestamp serverTimestamp = snapshot.GetValue("timestamp"); return serverTimestamp.ToDateTime(); } catch (Exception e) { Debug.LogError($"Failed to fetch server time: {e.Message}"); return DateTime.MinValue; } } /// /// Loads the daily reward data from Firebase and returns a structured object. /// If the document does not exist, creates a new one with the current server date. /// private async Task LoadDailyRewardsFromFirebase(string userId, DateTime serverDateTime) { DailyRewardsData result = new DailyRewardsData(); try { CollectionReference dailyRewardsCollection = firestore .Collection("Players") .Document(userId) .Collection("DailyRewards"); DocumentReference docRef = dailyRewardsCollection.Document("RewardData"); DocumentSnapshot snapshot = await docRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary data = snapshot.ToDictionary(); if (data.ContainsKey("firstClaimDate")) { Timestamp ts = (Timestamp)data["firstClaimDate"]; result.firstClaimDate = ts.ToDateTime(); } else { result.firstClaimDate = serverDateTime.Date; } if (data.ContainsKey("claimedRewards")) { result.claimedRewards = new List(); foreach (var obj in (List)data["claimedRewards"]) { result.claimedRewards.Add(Convert.ToInt32(obj)); } } else { result.claimedRewards = new List(); } } else { result.firstClaimDate = serverDateTime.Date; result.claimedRewards = new List(); Dictionary defaultData = new Dictionary { { "firstClaimDate", Timestamp.FromDateTime(serverDateTime.Date.ToUniversalTime()) }, { "claimedRewards", new List() } }; await docRef.SetAsync(defaultData); Debug.Log("Created new DailyRewards document with default data."); } } catch (Exception e) { Debug.LogError($"Error loading daily rewards: {e.Message}"); } return result; } /// /// Loads the new player reward data from Firebase and returns a structured object. /// private async Task LoadNewPlayerRewardsFromFirebase(string userId) { NewPlayerRewardsData result = new NewPlayerRewardsData(); try { DocumentReference docRef = firestore .Collection("Players") .Document(userId) .Collection("NewPlayerRewards") .Document("RewardData"); DocumentSnapshot snapshot = await docRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary data = snapshot.ToDictionary(); if (data.ContainsKey("accountCreationDate")) { Timestamp ts = (Timestamp)data["accountCreationDate"]; result.accountCreationDate = ts.ToDateTime(); } else { result.accountCreationDate = DateTime.Now.Date; } if (data.ContainsKey("claimedRewards")) { result.claimedRewards = new List(); foreach (var obj in (List)data["claimedRewards"]) { result.claimedRewards.Add(Convert.ToInt32(obj)); } } else { result.claimedRewards = new List(); } } else { result.accountCreationDate = DateTime.Now.Date; result.claimedRewards = new List(); } } catch (Exception e) { Debug.LogError($"Error loading new player rewards: {e.Message}"); } return result; } /// /// Loads the player's used coupons. /// private async Task LoadUsedCouponsAsync(string userId) { try { DocumentReference couponsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("UsedCoupons"); DocumentSnapshot snapshot = await couponsDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary couponsData = snapshot.ToDictionary(); List usedCoupons = new List(); foreach (var entry in couponsData) { usedCoupons.Add(entry.Key); } PlayerSave.SaveUsedCoupons(usedCoupons); } else { PlayerSave.SaveUsedCoupons(new List()); } } catch (Exception e) { Debug.LogError($"Failed to load used coupons: {e.Message}"); } } /// /// Retrieves the top 30 players with the highest scores. /// /// A list of dictionaries containing player data. public async Task>> GetTopPlayersAsync() { List> topPlayers = new List>(); try { CollectionReference playersRef = firestore.Collection("Players"); Query query = playersRef.OrderByDescending("score").Limit(30); QuerySnapshot querySnapshot = await query.GetSnapshotAsync(); foreach (DocumentSnapshot document in querySnapshot.Documents) { topPlayers.Add(document.ToDictionary()); } Debug.Log("Top players retrieved successfully."); } catch (System.Exception e) { Debug.LogError("Error retrieving top players: " + e.Message); } return topPlayers; } /// /// Loads the player's favorite character from the main document, parallel-ready. /// private async Task LoadPlayerCharacterFavouriteAsync(string userId) { try { DocumentReference docRef = firestore.Collection("Players").Document(userId); DocumentSnapshot snapshot = await docRef.GetSnapshotAsync(); if (snapshot.Exists && snapshot.ContainsField("PlayerCharacterFavourite")) { int favouriteCharacter = snapshot.GetValue("PlayerCharacterFavourite"); PlayerSave.SetFavouriteCharacter(favouriteCharacter); } else { await docRef.UpdateAsync("PlayerCharacterFavourite", 0); PlayerSave.SetFavouriteCharacter(0); } } catch (Exception e) { Debug.LogError($"Failed to load favourite character: {e.Message}"); } } /// /// Retrieves the player's current rank based on their score. /// /// The player's rank as an integer. public async Task GetPlayerRankAsync() { FirebaseUser user = auth.CurrentUser; if (user == null) { Debug.LogError("No user is logged in."); return -1; // Return an invalid rank if no user is logged in } try { // Retrieve all players ordered by score QuerySnapshot allPlayersSnapshot = await firestore.Collection("Players") .OrderByDescending("score") .GetSnapshotAsync(); int rank = 1; // Iterate through the players and find the rank of the current player foreach (DocumentSnapshot document in allPlayersSnapshot.Documents) { if (document.Id == user.UserId) { return rank; } rank++; } // If the player's ID was not found return -1; } catch (Exception e) { Debug.LogError("Failed to get player rank: " + e.Message); return -1; } } /// /// Loads the Battle Pass progress from Firebase and checks if the player's season is up-to-date. /// If the player's season data is missing, it will save the current season from the global settings. /// public async Task LoadBattlePassProgressAsync() { FirebaseUser user = auth.CurrentUser; if (user == null) { Debug.LogError("No user is logged in."); return; } try { // Reference to the Battle Pass progress and season documents DocumentReference battlePassDocRef = firestore .Collection("Players") .Document(user.UserId) .Collection("Progress") .Document("BattlePass"); DocumentReference seasonDocRef = firestore .Collection("Players") .Document(user.UserId) .Collection("Progress") .Document("Season"); // Get snapshots of the Battle Pass progress and season data DocumentSnapshot battlePassSnapshot = await battlePassDocRef.GetSnapshotAsync(); DocumentSnapshot seasonSnapshot = await seasonDocRef.GetSnapshotAsync(); if (battlePassSnapshot.Exists) { // Load Battle Pass XP, Level, and Unlock Status int currentXP = battlePassSnapshot.ContainsField("CurrentXP") ? battlePassSnapshot.GetValue("CurrentXP") : 0; int currentLevel = battlePassSnapshot.ContainsField("CurrentLevel") ? battlePassSnapshot.GetValue("CurrentLevel") : 1; bool isUnlocked = battlePassSnapshot.ContainsField("IsUnlocked") && battlePassSnapshot.GetValue("IsUnlocked"); // Set the Battle Pass manager values BattlePassManager.Singleton.currentXP = currentXP; BattlePassManager.Singleton.currentLevel = currentLevel; BattlePassManager.Singleton.xpForNextLevel = BattlePassManager.Singleton.CalculateXPForNextLevel(currentLevel); // Season management int currentSeason; if (!seasonSnapshot.Exists || !seasonSnapshot.ContainsField("CurrentSeason")) { currentSeason = await GetCurrentSeasonAsync(); await seasonDocRef.SetAsync(new Dictionary { { "CurrentSeason", currentSeason } }); Debug.Log($"Player's current season saved: {currentSeason}"); } else { currentSeason = seasonSnapshot.GetValue("CurrentSeason"); } // If the Battle Pass is unlocked, store that information locally if (isUnlocked) { PlayerPrefs.SetInt(BattlePassManager.Singleton.playerPrefsPassUnlockedKey, 1); } // Save XP and Level to PlayerPrefs PlayerPrefs.SetInt(BattlePassManager.Singleton.playerPrefsXPKey, currentXP); PlayerPrefs.SetInt(BattlePassManager.Singleton.playerPrefsLevelKey, currentLevel); // Set the current season in the BattlePassManager BattlePassManager.SetPlayerBattlePassSeason(currentSeason); // Save PlayerPrefs changes PlayerPrefs.Save(); Debug.Log("Battle Pass progress loaded from Firebase successfully."); } } catch (Exception e) { Debug.LogError("Failed to load Battle Pass progress: " + e.Message); } } /// /// Loads claimed rewards for the player's Battle Pass. /// private async Task LoadClaimedRewardsFromFirebase(string userId) { try { DocumentReference rewardsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("BattlePassRewards"); DocumentSnapshot snapshot = await rewardsDocRef.GetSnapshotAsync(); if (snapshot.Exists) { Dictionary rewardData = snapshot.ToDictionary(); foreach (KeyValuePair entry in rewardData) { string rewardId = entry.Key; bool isClaimed = (bool)entry.Value; if (isClaimed) { BattlePassManager.Singleton.MarkRewardAsClaimed(rewardId); } } PlayerPrefs.Save(); } } catch (Exception e) { Debug.LogError($"Failed to load claimed rewards: {e.Message}"); } } /// /// Resets Battle Pass progress in Firebase if you want to call it automatically. /// public async Task ResetBattlePassProgressAsync() { string userId = auth.CurrentUser.UserId; try { DocumentReference battlePassDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("BattlePass"); await battlePassDocRef.DeleteAsync(); DocumentReference battlePassRewardsDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("BattlePassRewards"); await battlePassRewardsDocRef.DeleteAsync(); int currentSeason = await GetCurrentSeasonAsync(); DocumentReference seasonDocRef = firestore .Collection("Players") .Document(userId) .Collection("Progress") .Document("Season"); await seasonDocRef.SetAsync(new Dictionary { { "CurrentSeason", currentSeason } }); BattlePassManager.SetPlayerBattlePassSeason(currentSeason); } catch (Exception e) { Debug.LogError($"Failed to reset Battle Pass: {e.Message}"); } } /// /// Retrieves the current Battle Pass season from the Firestore database. /// If the season document or field is missing, it returns a default value of 1. /// /// The current Battle Pass season as an integer. public async Task GetCurrentSeasonAsync() { try { // Reference to the document that contains the current season information DocumentReference seasonDocRef = firestore.Collection("BattlePass").Document("SeasonInfo"); DocumentSnapshot snapshot = await seasonDocRef.GetSnapshotAsync(); if (snapshot.Exists) { // Try to retrieve the value of the 'Season' field if (snapshot.TryGetValue("Season", out int currentSeason)) { Debug.Log($"Current Season from Firestore: {currentSeason}"); return currentSeason; } else { Debug.LogWarning("Season field is missing in Firestore document."); return 1; // Default value if the 'Season' field is missing } } else { Debug.LogWarning("Season document does not exist in Firestore."); return 1; // Default value if the document does not exist } } catch (System.Exception e) { Debug.LogError($"Failed to get current season: {e.Message}"); return 1; // Default value in case of an error } } /// /// Checks if the season has ended (parallel call if you want). /// private async Task CheckForSeasonEndAsync(string userId) { // You can remove user checks, as we did them in the main method try { DocumentReference seasonDocRef = firestore .Collection("BattlePass") .Document("SeasonInfo"); DocumentSnapshot snapshot = await seasonDocRef.GetSnapshotAsync(); if (snapshot.Exists && snapshot.ContainsField("StartSeason")) { Timestamp startSeasonTimestamp = snapshot.GetValue("StartSeason"); DateTime startSeasonDate = startSeasonTimestamp.ToDateTime(); int seasonLengthInDays = BattlePassManager.Singleton.SeasonLengthInDays; DateTime seasonEndDate = startSeasonDate.AddDays(seasonLengthInDays); BattlePassManager.Singleton.SaveRemainingSeasonTimeLocally(startSeasonDate); if (DateTime.UtcNow >= seasonEndDate) { Debug.Log("The current season has ended."); BattlePassManager.SetPlayerBattlePassSeasonEnded(); return true; } } return false; } catch (Exception e) { Debug.LogError($"Failed to check for season end: {e.Message}"); return false; } } /// /// Loads basic character info (skin, level, experience, mastery) from Firestore /// and applies to local PlayerSave. /// /// ID of the character. public async Task LoadCharacterBasicInfoAsync(int characterId) { if (auth.CurrentUser == null) { Debug.LogError("No user is logged in."); return; } try { string userId = auth.CurrentUser.UserId; DocumentReference charDocRef = firestore .Collection("Players") .Document(userId) .Collection("Characters") .Document(characterId.ToString()); DocumentSnapshot snapshot = await charDocRef.GetSnapshotAsync(); if (!snapshot.Exists) { Debug.LogWarning($"No character info found in Firestore for CharacterId {characterId}."); return; } // Apply to local PlayerSave if fields exist if (snapshot.ContainsField("CharacterSelectedSkin")) { int skinIndex = snapshot.GetValue("CharacterSelectedSkin"); PlayerSave.SetCharacterSkin(characterId, skinIndex); } if (snapshot.ContainsField("CharacterLevel")) { int level = snapshot.GetValue("CharacterLevel"); PlayerSave.SetCharacterLevel(characterId, level); } if (snapshot.ContainsField("CharacterCurrentExp")) { int currentExp = snapshot.GetValue("CharacterCurrentExp"); PlayerSave.SetCharacterCurrentExp(characterId, currentExp); } if (snapshot.ContainsField("CharacterMasteryLevel")) { int masteryLevel = snapshot.GetValue("CharacterMasteryLevel"); PlayerSave.SetCharacterMasteryLevel(characterId, masteryLevel); } if (snapshot.ContainsField("CharacterCurrentMasteryExp")) { int masteryExp = snapshot.GetValue("CharacterCurrentMasteryExp"); PlayerSave.SetCharacterCurrentMasteryExp(characterId, masteryExp); } Debug.Log($"Character basic info loaded for CharacterId {characterId}."); } catch (Exception e) { Debug.LogError($"Failed to load character basic info for {characterId}: {e.Message}"); } } public async Task LoadAndSyncAllCharacterBasicInfoAsync() { if (auth.CurrentUser == null) { Debug.LogError("No user is logged in."); return; } try { List tasks = new List(); foreach (CharacterData character in GameInstance.Singleton.characterData) { if (character.CheckUnlocked || MonetizationManager.Singleton.IsCharacterPurchased(character.characterId.ToString())) { tasks.Add(LoadCharacterBasicInfoAsync(character.characterId)); } } await Task.WhenAll(tasks); } catch (Exception e) { Debug.LogError("Failed to load all character info: " + e.Message); } } /// /// Loads the item documents for each known slot of each character in GameInstance.Singleton.characterData. /// For each 'slotName' in charData.itemSlots and charData.runeSlots, /// fetches the subcollection path: /Players/{userId}/Characters/{charId}/{slotName}. /// Then loads all docs found (usually only 1 doc, if you equip 1 item por slot). /// public async Task LoadCharacterSlotsAsync() { if (auth.CurrentUser == null) { Debug.LogError("No user is logged in."); return; } if (GameInstance.Singleton == null || GameInstance.Singleton.characterData == null) { Debug.LogError("GameInstance or characterData is null. Cannot load character slots."); return; } string userId = auth.CurrentUser.UserId; try { foreach (var charData in GameInstance.Singleton.characterData) { int charId = charData.characterId; if (charData.itemSlots != null) { foreach (string slotName in charData.itemSlots) { await LoadSlotDocsForCharacter(userId, charId, slotName); } } if (charData.runeSlots != null) { foreach (string slotName in charData.runeSlots) { await LoadSlotDocsForCharacter(userId, charId, slotName); } } Debug.Log($"[LoadCharacterSlotsAsync] Finished loading slots for Character {charId}."); } Debug.Log("[LoadCharacterSlotsAsync] Finished loading slots for ALL characters."); } catch (Exception e) { Debug.LogError($"[LoadCharacterSlotsAsync] Failed: {e.Message}"); } } /// /// Loads *all* documents in the slot subcollection of a single character. /// Path: /Players/{userId}/Characters/{charId}/{slotName} /// Each doc is typically a single item (uniqueItemGuid). /// private async Task LoadSlotDocsForCharacter(string userId, int charId, string slotName) { CollectionReference slotCol = firestore .Collection("Players") .Document(userId) .Collection("Characters") .Document(charId.ToString()) .Collection(slotName); QuerySnapshot snapshot = await slotCol.GetSnapshotAsync(); foreach (DocumentSnapshot doc in snapshot.Documents) { if (!doc.Exists) continue; string uniqueGuid = doc.Id; string itemId = doc.ContainsField("ItemId") ? doc.GetValue("ItemId") : ""; int itemLevel = doc.ContainsField("ItemLevel") ? doc.GetValue("ItemLevel") : 1; PlayerSave.SetCharacterSlotItem(charId, slotName, uniqueGuid); PlayerPrefs.SetInt($"{charId}_{slotName}_level", itemLevel); Debug.Log($"[LoadSlotDocsForCharacter] Loaded item '{itemId}' (GUID={uniqueGuid}) at slot '{slotName}', level={itemLevel}, for Char {charId}."); } } /// /// Loads all purchased items from Firestore into local MonetizationManager, /// then checks each InventoryItem in GameInstance. If 'isUnlocked' is true and /// no item with the same itemId exists, purchases it to create a new GUID. /// public async Task InitializeInventoryAsync() { if (auth.CurrentUser == null) { Debug.LogError("No user is logged in."); return; } List remotePurchasedItems = await LoadAllPurchasedInventoryItemsAsync(); var existingCharacters = MonetizationManager.Singleton.GetPurchasedCharacters(); var existingIcons = MonetizationManager.Singleton.GetPurchasedIcons(); var existingFrames = MonetizationManager.Singleton.GetPurchasedFrames(); var existingShopItems = MonetizationManager.Singleton.GetPurchasedShopItems(); MonetizationManager.Singleton.UpdatePurchasedItems( existingCharacters, existingIcons, existingFrames, existingShopItems, remotePurchasedItems ); bool needSaveItem = false; foreach (var soItem in GameInstance.Singleton.inventoryItems) { if (soItem.isUnlocked) { bool alreadyOwned = MonetizationManager.Singleton .GetPurchasedInventoryItems() .Exists(pi => pi.itemId == soItem.itemId); if (!alreadyOwned) { MonetizationManager.Singleton.PurchaseInventoryItemNoImmediateSave(soItem.itemId); Debug.Log($"Created new purchased item for {soItem.itemId} (isUnlocked)."); needSaveItem = true; } } } MonetizationManager.Singleton.SavePurchasedItems(needSaveItem); } /// /// Loads all purchased inventory items from Firestore into a List, ensuring that the parent document exists. /// public async Task> LoadAllPurchasedInventoryItemsAsync() { List result = new List(); try { string userId = auth.CurrentUser.UserId; // Reference to the "Items" document under PurchasedItems DocumentReference itemsDocRef = firestore .Collection("Players") .Document(userId) .Collection("PurchasedItems") .Document("Items"); // Check if the "Items" document exists; if not, create it. DocumentSnapshot itemsDocSnapshot = await itemsDocRef.GetSnapshotAsync(); if (!itemsDocSnapshot.Exists) { await itemsDocRef.SetAsync(new Dictionary()); Debug.Log("Created the 'Items' document as it did not exist."); } // Get the collection reference for the "List" subcollection CollectionReference itemsColRef = itemsDocRef.Collection("List"); QuerySnapshot snapshot = await itemsColRef.GetSnapshotAsync(); foreach (DocumentSnapshot doc in snapshot.Documents) { if (!doc.Exists) continue; string uniqueGuid = doc.Id; string itemId = doc.ContainsField("itemId") ? doc.GetValue("itemId") : ""; int itemLevel = doc.ContainsField("itemLevel") ? doc.GetValue("itemLevel") : 0; Dictionary upgrades = new Dictionary(); if (doc.ContainsField("itemUpgrades")) { Dictionary upgradesData = doc.GetValue>("itemUpgrades"); foreach (var kvp in upgradesData) { if (int.TryParse(kvp.Key, out int upgradeIndex)) { int upgradeValue = Convert.ToInt32(kvp.Value); upgrades[upgradeIndex] = upgradeValue; } } } PurchasedInventoryItem pi = new PurchasedInventoryItem { uniqueItemGuid = uniqueGuid, itemId = itemId, itemLevel = itemLevel, upgrades = upgrades }; result.Add(pi); } Debug.Log($"Loaded {result.Count} purchased inventory items from Firestore."); } catch (Exception e) { Debug.LogError($"Failed to load purchased inventory items: {e.Message}"); } return result; } /// /// Converts a list of generic objects to a list of a specific type using JsonUtility. /// private List ConvertToList(List data) { var list = new List(); if (data == null) return list; foreach (var item in data) { try { list.Add(JsonUtility.FromJson(item.ToString())); } catch (Exception ex) { Debug.LogError($"Error converting item: {ex.Message}"); } } return list; } } }