556 lines
15 KiB
C#
556 lines
15 KiB
C#
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 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 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.playerExperience <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
experience = Mathf.Max(0, bundle.playerExperience);
|
|
return true;
|
|
}
|
|
|
|
public static void SavePlayerExperience(int experience)
|
|
{
|
|
UpdateBundle(bundle => 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));
|
|
}
|
|
|
|
private static void UpdateBundle(Action<PlayerProgressBackupBundle> 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<string> 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<PlayerProgressBackupBundle>(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<AllyHeroDeployEntry>()
|
|
};
|
|
|
|
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<ExpBottleEntry>()
|
|
};
|
|
|
|
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<DushMaterialEntry>()
|
|
};
|
|
|
|
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<EquipmentConsumableEntry>()
|
|
};
|
|
|
|
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<StoreOwnershipEntry>()
|
|
};
|
|
|
|
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<int>(entry.unlockedStorySonIds)
|
|
: new List<int>()
|
|
});
|
|
}
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
}
|