using System; using System.Collections.Generic; using System.IO; using System.Text; using UnityEngine; [Serializable] public class PlayerProgressBackupBundle { public int version = 1; public long savedUtcTicks; public PlayerEconomyPayload economy; public bool hasPlayerExperience; public int playerExperience; public float bestOverallRks; public PlayerSkillSaveData playerSkill; public AllyHeroDeployLedgerPayload allyHeroDeploy; public ExpBottleLedgerPayload expBottle; public DushMaterialLedgerPayload dushMaterial; public EquipmentConsumableLedgerPayload equipmentConsumable; public StoreOwnershipPayload storeOwnership; } public static class PlayerProgressBackupService { private const string BackupFileName = "player_progress.bbackup"; private static PlayerProgressBackupBundle s_cachedBundle; private static bool s_cacheLoaded; private static bool s_isWriting; public static bool HasExistingBackupFile() { IReadOnlyList candidates = SaveIdentityUtility.GetPersistentRootVariants(); for (int i = 0; i < candidates.Count; i++) { string root = candidates[i]; if (string.IsNullOrWhiteSpace(root)) { continue; } if (File.Exists(Path.Combine(root, BackupFileName))) { return true; } } return false; } public static bool HasEconomyBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && bundle.economy != null; } public static bool HasPlayerExperienceBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && (bundle.hasPlayerExperience || bundle.playerExperience > 0); } public static bool HasAllyHeroDeployBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && bundle.allyHeroDeploy != null; } public static bool HasExpBottleBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && bundle.expBottle != null; } public static bool HasDushMaterialBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && bundle.dushMaterial != null; } public static bool HasEquipmentConsumableBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && bundle.equipmentConsumable != null; } public static bool HasStoreOwnershipBackup() { PlayerProgressBackupBundle bundle; return TryLoadBundle(out bundle) && bundle != null && bundle.storeOwnership != null; } public static bool TryRestoreEconomy(out PlayerEconomyPayload payload) { payload = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.economy == null) { return false; } payload = CloneEconomy(bundle.economy); return payload != null; } public static void SaveEconomy(PlayerEconomyPayload payload) { if (payload == null) { return; } UpdateBundle(bundle => bundle.economy = CloneEconomy(payload)); } public static bool TryRestorePlayerExperience(out int experience) { experience = 0; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null) { return false; } if (!bundle.hasPlayerExperience && bundle.playerExperience <= 0) { return false; } experience = Mathf.Max(0, bundle.playerExperience); return true; } public static void SavePlayerExperience(int experience) { UpdateBundle(bundle => { bundle.hasPlayerExperience = true; bundle.playerExperience = Mathf.Max(0, experience); }); } public static bool TryRestoreRks(out float rks) { rks = 0f; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.bestOverallRks <= 0f) { return false; } rks = Mathf.Max(0f, bundle.bestOverallRks); return true; } public static void SaveRks(float rks) { UpdateBundle(bundle => bundle.bestOverallRks = Mathf.Max(0f, rks)); } public static bool TryRestorePlayerSkill(out PlayerSkillSaveData saveData) { saveData = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.playerSkill == null) { return false; } saveData = ClonePlayerSkill(bundle.playerSkill); return saveData != null; } public static void SavePlayerSkill(PlayerSkillSaveData saveData) { if (saveData == null) { return; } UpdateBundle(bundle => bundle.playerSkill = ClonePlayerSkill(saveData)); } public static bool TryRestoreAllyHeroDeploy(out AllyHeroDeployLedgerPayload payload) { payload = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.allyHeroDeploy == null) { return false; } payload = CloneAllyHeroDeploy(bundle.allyHeroDeploy); return payload != null; } public static void SaveAllyHeroDeploy(AllyHeroDeployLedgerPayload payload) { if (payload == null) { return; } UpdateBundle(bundle => bundle.allyHeroDeploy = CloneAllyHeroDeploy(payload)); } public static bool TryRestoreExpBottle(out ExpBottleLedgerPayload payload) { payload = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.expBottle == null) { return false; } payload = CloneExpBottle(bundle.expBottle); return payload != null; } public static void SaveExpBottle(ExpBottleLedgerPayload payload) { if (payload == null) { return; } UpdateBundle(bundle => bundle.expBottle = CloneExpBottle(payload)); } public static bool TryRestoreDushMaterial(out DushMaterialLedgerPayload payload) { payload = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.dushMaterial == null) { return false; } payload = CloneDushMaterial(bundle.dushMaterial); return payload != null; } public static void SaveDushMaterial(DushMaterialLedgerPayload payload) { if (payload == null) { return; } UpdateBundle(bundle => bundle.dushMaterial = CloneDushMaterial(payload)); } public static bool TryRestoreEquipmentConsumable(out EquipmentConsumableLedgerPayload payload) { payload = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.equipmentConsumable == null) { return false; } payload = CloneEquipmentConsumable(bundle.equipmentConsumable); return payload != null; } public static void SaveEquipmentConsumable(EquipmentConsumableLedgerPayload payload) { if (payload == null) { return; } UpdateBundle(bundle => bundle.equipmentConsumable = CloneEquipmentConsumable(payload)); } public static bool TryRestoreStoreOwnership(out StoreOwnershipPayload payload) { payload = null; PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null || bundle.storeOwnership == null) { return false; } payload = CloneStoreOwnership(bundle.storeOwnership); return payload != null; } public static void SaveStoreOwnership(StoreOwnershipPayload payload) { if (payload == null) { return; } UpdateBundle(bundle => bundle.storeOwnership = CloneStoreOwnership(payload)); } public static void ClearPlayerExperienceBackup() { UpdateBundle(bundle => { bundle.hasPlayerExperience = false; bundle.playerExperience = 0; }); } public static void ClearRksBackup() { UpdateBundle(bundle => bundle.bestOverallRks = 0f); } public static void ClearPlayerSkillBackup() { UpdateBundle(bundle => bundle.playerSkill = null); } public static void ClearStoreOwnershipBackup() { UpdateBundle(bundle => bundle.storeOwnership = null); } public static void ClearAllBackupData() { s_cachedBundle = null; s_cacheLoaded = true; IReadOnlyList candidates = SaveIdentityUtility.GetPersistentRootVariants(); for (int i = 0; i < candidates.Count; i++) { string root = candidates[i]; if (string.IsNullOrWhiteSpace(root)) { continue; } string path = Path.Combine(root, BackupFileName); try { if (File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { Debug.LogWarning("[PlayerProgressBackup] Failed to delete backup '" + path + "': " + ex.Message); } } } private static void UpdateBundle(Action mutator) { if (mutator == null || s_isWriting) { return; } PlayerProgressBackupBundle bundle; if (!TryLoadBundle(out bundle) || bundle == null) { bundle = CreateDefaultBundle(); } mutator(bundle); bundle.savedUtcTicks = DateTime.UtcNow.Ticks; SaveBundle(bundle); } private static bool TryLoadBundle(out PlayerProgressBackupBundle bundle) { if (s_cacheLoaded) { bundle = s_cachedBundle; return bundle != null; } s_cacheLoaded = true; s_cachedBundle = null; IReadOnlyList candidates = SaveIdentityUtility.GetPersistentRootVariants(); for (int i = 0; i < candidates.Count; i++) { string root = candidates[i]; if (string.IsNullOrWhiteSpace(root)) { continue; } string path = Path.Combine(root, BackupFileName); if (!File.Exists(path)) { continue; } try { string json = File.ReadAllText(path, Encoding.UTF8); if (string.IsNullOrWhiteSpace(json)) { continue; } PlayerProgressBackupBundle loaded = JsonUtility.FromJson(json); if (loaded == null) { continue; } s_cachedBundle = loaded; break; } catch (Exception ex) { Debug.LogWarning("[PlayerProgressBackup] Failed to read backup '" + path + "': " + ex.Message); } } bundle = s_cachedBundle; return bundle != null; } private static void SaveBundle(PlayerProgressBackupBundle bundle) { if (bundle == null) { return; } string root = SaveIdentityUtility.GetCanonicalPersistentRoot(); string path = Path.Combine(root, BackupFileName); try { s_isWriting = true; Directory.CreateDirectory(root); string json = JsonUtility.ToJson(bundle, false); File.WriteAllText(path, json, Encoding.UTF8); s_cachedBundle = bundle; s_cacheLoaded = true; } catch (Exception ex) { Debug.LogWarning("[PlayerProgressBackup] Failed to save backup '" + path + "': " + ex.Message); } finally { s_isWriting = false; } } private static PlayerProgressBackupBundle CreateDefaultBundle() { return new PlayerProgressBackupBundle { version = 1, savedUtcTicks = DateTime.UtcNow.Ticks }; } private static PlayerEconomyPayload CloneEconomy(PlayerEconomyPayload source) { if (source == null) { return null; } return new PlayerEconomyPayload { version = source.version, lastUpdatedUtcTicks = source.lastUpdatedUtcTicks, coins = source.coins, material = source.material }; } private static PlayerSkillSaveData ClonePlayerSkill(PlayerSkillSaveData source) { if (source == null) { return null; } return new PlayerSkillSaveData { selectedSkillIndex = source.selectedSkillIndex, postMatchRewardCounter = source.postMatchRewardCounter, skillSwitchCooldownRemainingMatches = source.skillSwitchCooldownRemainingMatches }; } private static AllyHeroDeployLedgerPayload CloneAllyHeroDeploy(AllyHeroDeployLedgerPayload source) { if (source == null) { return null; } var clone = new AllyHeroDeployLedgerPayload { version = source.version, lastUpdatedUtcTicks = source.lastUpdatedUtcTicks, entries = new List() }; if (source.entries != null) { for (int i = 0; i < source.entries.Count; i++) { AllyHeroDeployEntry entry = source.entries[i]; if (entry == null) { continue; } clone.entries.Add(new AllyHeroDeployEntry { heroId = entry.heroId, currentExp = entry.currentExp, unlockedTierIndex = entry.unlockedTierIndex, levelLock = entry.levelLock, autoBreakthroughEnabled = entry.autoBreakthroughEnabled, deployCount = entry.deployCount, finishCount = entry.finishCount, mvpCount = entry.mvpCount, joinDateUtcTicks = entry.joinDateUtcTicks }); } } return clone; } private static ExpBottleLedgerPayload CloneExpBottle(ExpBottleLedgerPayload source) { if (source == null) { return null; } var clone = new ExpBottleLedgerPayload { version = source.version, lastUpdatedUtcTicks = source.lastUpdatedUtcTicks, entries = new List() }; if (source.entries != null) { for (int i = 0; i < source.entries.Count; i++) { ExpBottleEntry entry = source.entries[i]; if (entry == null) { continue; } clone.entries.Add(new ExpBottleEntry { key = entry.key, count = entry.count }); } } return clone; } private static DushMaterialLedgerPayload CloneDushMaterial(DushMaterialLedgerPayload source) { if (source == null) { return null; } var clone = new DushMaterialLedgerPayload { version = source.version, lastUpdatedUtcTicks = source.lastUpdatedUtcTicks, entries = new List() }; if (source.entries != null) { for (int i = 0; i < source.entries.Count; i++) { DushMaterialEntry entry = source.entries[i]; if (entry == null) { continue; } clone.entries.Add(new DushMaterialEntry { key = entry.key, count = entry.count }); } } return clone; } private static EquipmentConsumableLedgerPayload CloneEquipmentConsumable(EquipmentConsumableLedgerPayload source) { if (source == null) { return null; } var clone = new EquipmentConsumableLedgerPayload { version = source.version, lastUpdatedUtcTicks = source.lastUpdatedUtcTicks, entries = new List() }; if (source.entries != null) { for (int i = 0; i < source.entries.Count; i++) { EquipmentConsumableEntry entry = source.entries[i]; if (entry == null) { continue; } clone.entries.Add(new EquipmentConsumableEntry { key = entry.key, count = entry.count }); } } return clone; } private static StoreOwnershipPayload CloneStoreOwnership(StoreOwnershipPayload source) { if (source == null) { return null; } var clone = new StoreOwnershipPayload { version = source.version, lastUpdatedUtcTicks = source.lastUpdatedUtcTicks, entries = new List() }; if (source.entries != null) { for (int i = 0; i < source.entries.Count; i++) { StoreOwnershipEntry entry = source.entries[i]; if (entry == null) { continue; } clone.entries.Add(new StoreOwnershipEntry { storeItemId = entry.storeItemId, owned = entry.owned, unlockedStorySonIds = entry.unlockedStorySonIds != null ? new List(entry.unlockedStorySonIds) : new List() }); } } return clone; } }