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