客户端rsa,冗余清理,bug修复,安卓build问题
This commit is contained in:
@@ -20,6 +20,7 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
|
||||
private readonly Dictionary<int, long> joinDateUtcTicksByHeroId = new Dictionary<int, long>();
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
@@ -78,14 +79,18 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
|
||||
|
||||
AllyHeroDeployLedgerPayload payload;
|
||||
loadedFromSave = AllyHeroDeployLedgerStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || AllyHeroDeployLedgerStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
initialized = true;
|
||||
if (!loadedFromSave)
|
||||
if (!loadedFromSave && !hasAnyRecoverableLocalState)
|
||||
{
|
||||
ResetGrowthStateToDefaults();
|
||||
}
|
||||
SyncAllMirrorFlags();
|
||||
SaveNow();
|
||||
if (loadedFromSave || !hasAnyRecoverableLocalState)
|
||||
{
|
||||
SaveNow();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetToDefaultsIfSaveMissing()
|
||||
@@ -95,7 +100,7 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (AllyHeroDeployLedgerStorage.HasExistingSaveFile())
|
||||
if (hasAnyRecoverableLocalState || AllyHeroDeployLedgerStorage.HasExistingSaveFile())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class AllyHeroDeployLedgerStorage
|
||||
private const string MainFileName = ".ahd.dat";
|
||||
private const string BackupFileName = ".ahd.bak";
|
||||
private const string TempFileName = ".ahd.tmp";
|
||||
private const string RecoverySlotKey = "ally_hero_deploy_storage";
|
||||
|
||||
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName);
|
||||
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
|
||||
@@ -19,25 +20,7 @@ public static class AllyHeroDeployLedgerStorage
|
||||
|
||||
public static bool HasExistingSaveFile()
|
||||
{
|
||||
var mainCandidates = SaveIdentityUtility.GetVaultFilePathVariants(MainFileName);
|
||||
for (int i = 0; i < mainCandidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(mainCandidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
var backupCandidates = SaveIdentityUtility.GetVaultFilePathVariants(BackupFileName);
|
||||
for (int i = 0; i < backupCandidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(backupCandidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return HasAnyRecoverableState();
|
||||
}
|
||||
|
||||
public static bool TryLoad(out AllyHeroDeployLedgerPayload payload)
|
||||
@@ -61,9 +44,23 @@ public static class AllyHeroDeployLedgerStorage
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState()
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasAllyHeroDeployBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
public static bool TrySave(AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
try
|
||||
@@ -85,6 +82,7 @@ public static class AllyHeroDeployLedgerStorage
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveAllyHeroDeploy(payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -177,6 +175,20 @@ public static class AllyHeroDeployLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasAnyVaultFile(string fileName)
|
||||
{
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(candidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(AllyHeroDeployLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
#if !UNITY_WEBGL
|
||||
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
|
||||
using Steamworks;
|
||||
#endif
|
||||
|
||||
@@ -118,6 +118,34 @@ public static class DlcOwnershipService
|
||||
Cache.Clear();
|
||||
}
|
||||
|
||||
public static void ClearLocalPersistence()
|
||||
{
|
||||
dlcData[] allDlcs = RuntimeResourcesCache.LoadAllDlcs();
|
||||
if (allDlcs != null)
|
||||
{
|
||||
for (int i = 0; i < allDlcs.Length; i++)
|
||||
{
|
||||
dlcData dlc = allDlcs[i];
|
||||
if (dlc == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = dlc.GetResolvedDlcKey();
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PlayerPrefs.DeleteKey(RemoteOwnershipPrefsPrefix + key);
|
||||
PlayerPrefs.DeleteKey(LocalOverridePrefsPrefix + key);
|
||||
}
|
||||
}
|
||||
|
||||
PlayerPrefs.Save();
|
||||
Cache.Clear();
|
||||
}
|
||||
|
||||
private static DlcEntitlementState ResolveEntitlement(dlcData dlc, string key)
|
||||
{
|
||||
if (!dlc.ShouldEnforceEntitlement())
|
||||
@@ -137,7 +165,7 @@ public static class DlcOwnershipService
|
||||
return new DlcEntitlementState(key, true, true, DlcEntitlementSource.LocalFlag);
|
||||
}
|
||||
|
||||
#if !UNITY_WEBGL
|
||||
#if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX
|
||||
if (dlc.steamAppId > 0 && SteamManager.Initialized)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -39,7 +39,7 @@ public static class DlcRemoteManifestSyncService
|
||||
}
|
||||
}
|
||||
|
||||
private const string DefaultServerUrl = "http://47.112.187.172:8080";
|
||||
private const string DefaultServerUrl = "https://game.bansonic.top";
|
||||
private const string RemoteManifestFilePrefix = "remote_";
|
||||
private const string ManifestApiPath = "/api/dlcs/manifests";
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class DushMaterialLedger : MonoBehaviour
|
||||
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
private Player_SO boundPlayerData;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
@@ -68,11 +69,23 @@ public sealed class DushMaterialLedger : MonoBehaviour
|
||||
|
||||
DushMaterialLedgerPayload payload;
|
||||
loadedFromSave = DushMaterialLedgerStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || DushMaterialLedgerStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
initialized = true;
|
||||
TryRecoverFromDefaultPlayerData();
|
||||
SyncMirrorCounts();
|
||||
SaveNow();
|
||||
if (!loadedFromSave && !hasAnyRecoverableLocalState)
|
||||
{
|
||||
bool recoveredFromMirror = TryRecoverFromDefaultPlayerData();
|
||||
if (recoveredFromMirror)
|
||||
{
|
||||
hasAnyRecoverableLocalState = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedFromSave || !hasAnyRecoverableLocalState)
|
||||
{
|
||||
SaveNow();
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachPlayerData(Player_SO playerData)
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class DushMaterialLedgerStorage
|
||||
private const string MainFileName = ".dsm.dat";
|
||||
private const string BackupFileName = ".dsm.bak";
|
||||
private const string TempFileName = ".dsm.tmp";
|
||||
private const string RecoverySlotKey = "dush_material_ledger_storage";
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
@@ -53,9 +54,23 @@ public static class DushMaterialLedgerStorage
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState()
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasDushMaterialBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
public static bool TrySave(DushMaterialLedgerPayload payload)
|
||||
{
|
||||
try
|
||||
@@ -77,6 +92,7 @@ public static class DushMaterialLedgerStorage
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveDushMaterial(payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -169,6 +185,20 @@ public static class DushMaterialLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasAnyVaultFile(string fileName)
|
||||
{
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(candidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(DushMaterialLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
private const string MainFileName = ".eqc.dat";
|
||||
private const string BackupFileName = ".eqc.bak";
|
||||
private const string TempFileName = ".eqc.tmp";
|
||||
private const string RecoverySlotKey = "equipment_consumable_ledger_storage";
|
||||
|
||||
private static string VaultDirectoryPath => Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName);
|
||||
private static string MainFilePath => Path.Combine(VaultDirectoryPath, MainFileName);
|
||||
@@ -38,9 +39,22 @@ public static class EquipmentConsumableLedgerStorage
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState()
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
public static bool TrySave(EquipmentConsumableLedgerPayload payload)
|
||||
{
|
||||
try
|
||||
@@ -62,6 +76,7 @@ public static class EquipmentConsumableLedgerStorage
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveEquipmentConsumable(payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -81,6 +96,20 @@ public static class EquipmentConsumableLedgerStorage
|
||||
};
|
||||
}
|
||||
|
||||
private static bool HasAnyVaultFile(string fileName)
|
||||
{
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(candidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryReadPayload(string path, out EquipmentConsumableLedgerPayload payload)
|
||||
{
|
||||
payload = CreateDefaultPayload();
|
||||
|
||||
@@ -12,6 +12,7 @@ public sealed class ExpBottleLedger : MonoBehaviour
|
||||
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
private Player_SO boundPlayerData;
|
||||
|
||||
public bool IsReady
|
||||
@@ -74,11 +75,24 @@ public sealed class ExpBottleLedger : MonoBehaviour
|
||||
|
||||
ExpBottleLedgerPayload payload;
|
||||
loadedFromSave = ExpBottleLedgerStorage.TryLoad(out payload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || ExpBottleLedgerStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
initialized = true;
|
||||
TryRecoverFromDefaultPlayerData();
|
||||
SyncMirrorCounts();
|
||||
SaveNow();
|
||||
if (!loadedFromSave && !hasAnyRecoverableLocalState)
|
||||
{
|
||||
bool recoveredFromMirror = TryRecoverFromDefaultPlayerData();
|
||||
if (recoveredFromMirror)
|
||||
{
|
||||
hasAnyRecoverableLocalState = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedFromSave || !hasAnyRecoverableLocalState)
|
||||
{
|
||||
SaveNow();
|
||||
}
|
||||
|
||||
if (OnLedgerReloaded != null)
|
||||
{
|
||||
OnLedgerReloaded();
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class ExpBottleLedgerStorage
|
||||
private const string MainFileName = ".xpv.dat";
|
||||
private const string BackupFileName = ".xpv.bak";
|
||||
private const string TempFileName = ".xpv.tmp";
|
||||
private const string RecoverySlotKey = "exp_bottle_ledger_storage";
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
@@ -53,9 +54,23 @@ public static class ExpBottleLedgerStorage
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState()
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasExpBottleBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
public static bool TrySave(ExpBottleLedgerPayload payload)
|
||||
{
|
||||
try
|
||||
@@ -77,6 +92,7 @@ public static class ExpBottleLedgerStorage
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveExpBottle(payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -169,6 +185,20 @@ public static class ExpBottleLedgerStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasAnyVaultFile(string fileName)
|
||||
{
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(candidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(ExpBottleLedgerPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UnityEngine;
|
||||
|
||||
public static class FirstRunFactoryResetService
|
||||
{
|
||||
private const string StoreStateLegacyPath = "storeSystem_state.json";
|
||||
// 明文"已初始化"标记:不依赖 deviceUniqueIdentifier / 加密,仅表示本机曾成功初始化过。
|
||||
// 用途见 TryPerformFactoryResetFallback:区分"真首启"与"存档存在但暂时读不出"。
|
||||
private const string InitializedMarkerFileName = ".bansonic_initialized";
|
||||
private static readonly int[] DefaultTeamHeroIds = { 30201, 30202, 30203, 30204, 30205 };
|
||||
private static bool executed;
|
||||
|
||||
private static string InitializedMarkerPath =>
|
||||
Path.Combine(Application.persistentDataPath, InitializedMarkerFileName);
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
private static void Bootstrap()
|
||||
{
|
||||
TryPerformFactoryResetFallback();
|
||||
}
|
||||
|
||||
private static void TryPerformFactoryResetFallback()
|
||||
{
|
||||
if (executed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
executed = true;
|
||||
|
||||
if (HasAnyRecoverableLocalState())
|
||||
{
|
||||
// 有可恢复存档:补写标记(覆盖历史版本升级上来、尚无标记的老用户),不重置。
|
||||
EnsureInitializedMarker();
|
||||
return;
|
||||
}
|
||||
|
||||
// 走到这里说明"当前读不到任何可恢复存档"。必须区分两种情况:
|
||||
// A) 真正首次运行:应用出厂默认(全 0)并写标记 —— 安全。
|
||||
// B) 曾经玩过、但存档暂时读不出(典型:deviceUniqueIdentifier 变化致解密/验签失败):
|
||||
// 此时清零 = 数据丢失灾难。宁可保留现状等待其它恢复路径,也绝不主动抹除。
|
||||
if (HasInitializedMarker())
|
||||
{
|
||||
Debug.LogWarning("[FactoryReset] 检测到已初始化标记,但当前读不到任何存档。" +
|
||||
"为避免误删既有(暂不可读)玩家数据,跳过出厂重置。");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.LogWarning("[FactoryReset] 首次运行且无可恢复存档,应用出厂默认设置。");
|
||||
ApplyFactoryReset();
|
||||
EnsureInitializedMarker();
|
||||
}
|
||||
|
||||
private static bool HasInitializedMarker()
|
||||
{
|
||||
try
|
||||
{
|
||||
return File.Exists(InitializedMarkerPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[FactoryReset] 读取初始化标记失败: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureInitializedMarker()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(InitializedMarkerPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllText(InitializedMarkerPath, DateTime.UtcNow.ToString("o"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning("[FactoryReset] 写入初始化标记失败: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasAnyRecoverableLocalState()
|
||||
{
|
||||
if (PlayerEconomyStorage.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SecureSaveVault.HasAnyRecoverableState("player_experience", "runtime", Path.Combine(Application.persistentDataPath, "player_experience.json")))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (AllyHeroDeployLedgerStorage.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ExpBottleLedgerStorage.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DushMaterialLedgerStorage.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (EquipmentConsumableLedgerStorage.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StoreOwnershipStorage.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SecureSaveVault.HasAnyRecoverableState("player_rks", "overall"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SecureSaveVault.HasAnyRecoverableState("player_skills", "runtime"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SecureSaveVault.HasAnyRecoverableState("recent_play_history", "runs_v1"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SecureSaveVault.HasAnyRecoverableState("runtime_equipment", "generated_list"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (DailyTaskSaveService.HasAnyRecoverableState())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SecureSaveVault.HasAnyRecoverableState("store_state", "runtime", Path.Combine(Application.persistentDataPath, StoreStateLegacyPath)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (PlayerProgressBackupService.HasExistingBackupFile())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
SongData[] songs = RuntimeResourcesCache.LoadAllSongs();
|
||||
if (songs != null)
|
||||
{
|
||||
for (int i = 0; i < songs.Length; i++)
|
||||
{
|
||||
SongData song = songs[i];
|
||||
if (song == null || song.songID <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string legacySongPath = Path.Combine(Application.persistentDataPath, $"SongData_{song.songID}.json");
|
||||
if (SecureSaveVault.HasAnyRecoverableState("song_runtime", song.songID.ToString(), legacySongPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ApplyFactoryReset()
|
||||
{
|
||||
ResetPlayerEconomyAndExperience();
|
||||
ResetHeroGrowthAndLoadout();
|
||||
ResetPlayerSkillAndRks();
|
||||
ResetSongProgressAndRecentHistory();
|
||||
ResetDailyTasks();
|
||||
ResetStoreState();
|
||||
ResetGeneratedEquipment();
|
||||
ResetTeamSelection();
|
||||
ResetSettingsAndUserPrefs();
|
||||
ResetPlayerMirrorSo();
|
||||
ResetDlcOwnershipFlags();
|
||||
|
||||
RuntimeResourcesCache.InvalidateAll();
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private static void ResetPlayerEconomyAndExperience()
|
||||
{
|
||||
Player_SO player = RuntimeResourcesCache.LoadDefaultPlayerSo();
|
||||
|
||||
PlayerEconomyLedger economyLedger = PlayerEconomyLedger.EnsureInstance();
|
||||
economyLedger.SetCoins(0);
|
||||
economyLedger.SetMaterial(0);
|
||||
|
||||
PlayerExperienceLedger experienceLedger = PlayerExperienceLedger.EnsureInstance();
|
||||
experienceLedger.SetExperience(0);
|
||||
|
||||
if (player != null)
|
||||
{
|
||||
player.player_currentEXP = 0;
|
||||
player.SetCoins(0);
|
||||
player.SetMaterial(0);
|
||||
player.SetURankingScore(0f);
|
||||
player.SetLegacyExpBottleCount("commonExpBottle78001", 0);
|
||||
player.SetLegacyExpBottleCount("mediumExpBottle78002", 0);
|
||||
player.SetLegacyExpBottleCount("superiorExpBottle78003", 0);
|
||||
player.SetLegacyExpBottleCount("supremeExpBottle78004", 0);
|
||||
player.SetLegacyExpBottleCount("extraordinaryExpBottle78005", 0);
|
||||
player.SetLegacyExpBottleCount("celestialExpBottle78006", 0);
|
||||
player.SetLegacyDushMaterialCount("dushMaterial78021", 0);
|
||||
player.SetLegacyDushMaterialCount("dushMaterial78022", 0);
|
||||
player.SetLegacyDushMaterialCount("dushMaterial78023", 0);
|
||||
player.SetLegacyDushMaterialCount("dushMaterial78024", 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetHeroGrowthAndLoadout()
|
||||
{
|
||||
AllyHeroDeployLedger.EnsureInstance().ResetGrowthProgressOnly();
|
||||
ExpBottleLedger.EnsureInstance().ResetAllToZero();
|
||||
DushMaterialLedger.EnsureInstance().ResetAllToZero();
|
||||
EquipmentConsumableLedger.EnsureInstance().ResetAllToZero();
|
||||
AllyHero_SO.ClearAllEquippedSkills();
|
||||
|
||||
AllyHero_SO[] heroes = RuntimeResourcesCache.LoadAllAllyHeroes();
|
||||
if (heroes == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < heroes.Length; i++)
|
||||
{
|
||||
AllyHero_SO hero = heroes[i];
|
||||
if (hero == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
hero.ClearEquippedEquipment();
|
||||
hero.ClearSelectedSkin();
|
||||
hero.ally_currentEXP = 0;
|
||||
hero.ally_growthUnlockedTierIndex = 0;
|
||||
hero.level_lock = false;
|
||||
hero.ally_autoBreakthroughEnabled = false;
|
||||
hero.ally_battleDeployCount = 0;
|
||||
hero.ally_finishCount = 0;
|
||||
hero.ally_mvpCount = 0;
|
||||
hero.ally_joinDateUtcTicks = 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetPlayerSkillAndRks()
|
||||
{
|
||||
PlayerRksService.ClearPersistentState();
|
||||
PlayerSkillService.ResetToDefaultLevelOneSkill();
|
||||
}
|
||||
|
||||
private static void ResetSongProgressAndRecentHistory()
|
||||
{
|
||||
SongData[] songs = RuntimeResourcesCache.LoadAllSongs();
|
||||
if (songs != null)
|
||||
{
|
||||
for (int i = 0; i < songs.Length; i++)
|
||||
{
|
||||
if (songs[i] != null)
|
||||
{
|
||||
songs[i].ClearSaveDataAndReload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RecentPlayHistoryStore.Clear();
|
||||
}
|
||||
|
||||
private static void ResetDailyTasks()
|
||||
{
|
||||
DailyTaskService.EnsureInstance().ClearPersistentState();
|
||||
}
|
||||
|
||||
private static void ResetStoreState()
|
||||
{
|
||||
SecureSaveVault.Delete("store_state", "runtime", Path.Combine(Application.persistentDataPath, StoreStateLegacyPath));
|
||||
StoreOwnershipLedger.EnsureInstance().ClearPersistentState();
|
||||
PlayerProgressBackupService.ClearStoreOwnershipBackup();
|
||||
|
||||
storeItemSO[] storeItems = RuntimeResourcesCache.LoadAllStoreItems();
|
||||
if (storeItems == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < storeItems.Length; i++)
|
||||
{
|
||||
storeItemSO item = storeItems[i];
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
item.purchasedCount = 0;
|
||||
item.user_has_read = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetGeneratedEquipment()
|
||||
{
|
||||
equipSmelt.ClearPersistentState();
|
||||
Bansonic.equipmentGenerator.ClearRuntimeGeneratedPersistence();
|
||||
PlayerPrefs.DeleteKey("bansonic_equipment_next_id_v1");
|
||||
}
|
||||
|
||||
private static void ResetTeamSelection()
|
||||
{
|
||||
for (int i = 0; i < DefaultTeamHeroIds.Length; i++)
|
||||
{
|
||||
int slot = i + 1;
|
||||
PlayerPrefs.SetInt($"selected_heroSlot0{slot}_heroID", DefaultTeamHeroIds[i]);
|
||||
PlayerPrefs.DeleteKey($"selected_heroSlot0{slot}_exp");
|
||||
}
|
||||
|
||||
PlayerPrefs.SetInt("SelectedMainHeroID", DefaultTeamHeroIds[0]);
|
||||
}
|
||||
|
||||
private static void ResetSettingsAndUserPrefs()
|
||||
{
|
||||
string[] keysToDelete =
|
||||
{
|
||||
"noteSpeedMultiplier",
|
||||
"UserGlobalDelaySeconds",
|
||||
"screenMode",
|
||||
"resolutionIndex",
|
||||
"customResW",
|
||||
"customResH",
|
||||
"frameRateIndex",
|
||||
"SuperResolution",
|
||||
"EnableGlobalMute",
|
||||
"Volume_Main",
|
||||
"Volume_NoteHit",
|
||||
"Volume_MusicInGame",
|
||||
"Volume_MusicOutGame",
|
||||
"Volume_UI",
|
||||
"EnableSyncNotePrefab",
|
||||
"bansonic_language_code",
|
||||
"global_chat_last_private_partner_id",
|
||||
"notebook.selected_category",
|
||||
"notebook.selected_father_id",
|
||||
"notebook.selected_son_id",
|
||||
"bansonic_online_enabled",
|
||||
"before_everything_agree_ranking",
|
||||
"before_everything_first_launch_completed",
|
||||
"before_everything_light_warning_accepted",
|
||||
"before_everything_user_info_accepted",
|
||||
"selected_song_last_entered_id",
|
||||
"song_select_skip_quick_enter_prompt"
|
||||
};
|
||||
|
||||
for (int i = 0; i < keysToDelete.Length; i++)
|
||||
{
|
||||
PlayerPrefs.DeleteKey(keysToDelete[i]);
|
||||
}
|
||||
|
||||
string[] keyBindingPrefixes = { "red", "green", "yellow", "purple", "blue" };
|
||||
for (int i = 0; i < keyBindingPrefixes.Length; i++)
|
||||
{
|
||||
PlayerPrefs.DeleteKey($"KeyBinding_{keyBindingPrefixes[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetPlayerMirrorSo()
|
||||
{
|
||||
Player_SO player = RuntimeResourcesCache.LoadDefaultPlayerSo();
|
||||
if (player == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
player.SetCurrentLevel(0);
|
||||
player.player_currentEXP = 0;
|
||||
}
|
||||
|
||||
private static void ResetDlcOwnershipFlags()
|
||||
{
|
||||
DlcOwnershipService.ClearLocalPersistence();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0eee97e142264d24a89205928ea673a8
|
||||
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
public static class LocalRecoveryMirror
|
||||
{
|
||||
private const string RecoveryDirectoryName = ".save_recovery";
|
||||
|
||||
public static void SaveJson<T>(string slotKey, T data)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slotKey) || (!typeof(T).IsValueType && (object)data == null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = JsonUtility.ToJson(data, false);
|
||||
SaveRaw(slotKey, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LocalRecoveryMirror] SaveJson failed ({slotKey}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryLoadJson<T>(string slotKey, out T data)
|
||||
{
|
||||
data = default(T);
|
||||
if (string.IsNullOrWhiteSpace(slotKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string json;
|
||||
if (!TryLoadRaw(slotKey, out json) || string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
data = JsonUtility.FromJson<T>(json);
|
||||
if (typeof(T).IsValueType)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return (object)data != null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LocalRecoveryMirror] TryLoadJson failed ({slotKey}): {ex.Message}");
|
||||
data = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SaveRaw(string slotKey, string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slotKey) || json == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string path = GetCanonicalPath(slotKey);
|
||||
string directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string backupPath = path + ".bak";
|
||||
string tempPath = path + ".tmp";
|
||||
File.WriteAllText(tempPath, json, Encoding.UTF8);
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Copy(path, backupPath, true);
|
||||
}
|
||||
|
||||
File.Copy(tempPath, path, true);
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LocalRecoveryMirror] SaveRaw failed ({slotKey}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryLoadRaw(string slotKey, out string json)
|
||||
{
|
||||
json = null;
|
||||
if (string.IsNullOrWhiteSpace(slotKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> candidates = GetVariantPaths(slotKey);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
string path = candidates[i];
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string loaded = File.ReadAllText(path, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(loaded))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
json = loaded;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LocalRecoveryMirror] TryLoadRaw failed ({path}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasSlotData(string slotKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slotKey))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> candidates = GetVariantPaths(slotKey);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
string path = candidates[i];
|
||||
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void DeleteSlot(string slotKey)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(slotKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> candidates = GetVariantPaths(slotKey);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
string path = candidates[i];
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[LocalRecoveryMirror] DeleteSlot failed ({path}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetCanonicalPath(string slotKey)
|
||||
{
|
||||
string fileName = BuildFileName(slotKey);
|
||||
return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), RecoveryDirectoryName, fileName);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetVariantPaths(string slotKey)
|
||||
{
|
||||
var result = new List<string>();
|
||||
string fileName = BuildFileName(slotKey);
|
||||
string backupFileName = fileName + ".bak";
|
||||
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
string root = roots[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddDistinct(result, Path.Combine(root, RecoveryDirectoryName, fileName));
|
||||
AddDistinct(result, Path.Combine(root, RecoveryDirectoryName, backupFileName));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string BuildFileName(string slotKey)
|
||||
{
|
||||
return slotKey.Trim().Replace('/', '_').Replace('\\', '_').Replace(':', '_') + ".json";
|
||||
}
|
||||
|
||||
private static void AddDistinct(List<string> target, string value)
|
||||
{
|
||||
if (target == null || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < target.Count; i++)
|
||||
{
|
||||
if (string.Equals(target[i], value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
target.Add(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d25f7b0a830e50741b49abd6263782bc
|
||||
@@ -11,6 +11,7 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
|
||||
private PlayerEconomyPayload payload;
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
private Player_SO boundPlayerData;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
@@ -69,13 +70,18 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
|
||||
|
||||
PlayerEconomyPayload loadedPayload;
|
||||
loadedFromSave = PlayerEconomyStorage.TryLoad(out loadedPayload);
|
||||
hasAnyRecoverableLocalState = loadedFromSave || PlayerEconomyStorage.HasAnyRecoverableState();
|
||||
payload = loadedPayload;
|
||||
if (payload == null)
|
||||
{
|
||||
payload = PlayerEconomyStorage.CreateDefaultPayload();
|
||||
}
|
||||
initialized = true;
|
||||
SaveNow();
|
||||
|
||||
if (loadedFromSave)
|
||||
{
|
||||
SaveNow();
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachPlayerData(Player_SO playerData)
|
||||
@@ -88,12 +94,17 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
|
||||
InitializeIfNeeded();
|
||||
boundPlayerData = playerData;
|
||||
|
||||
if (!loadedFromSave)
|
||||
if (!loadedFromSave && !hasAnyRecoverableLocalState)
|
||||
{
|
||||
payload.coins = Mathf.Max(0, playerData.Coins);
|
||||
payload.material = Mathf.Max(0, playerData.Material);
|
||||
// 首次启动且无任何可恢复存档:用硬编码默认值(0)建立初始存档,
|
||||
// 绝不读取 Player_SO 的烘焙值——该资产可能被打包时的测试数据污染,
|
||||
// 直接采用会把玩家进度“种”成 999988 之类的测试数值。
|
||||
payload = PlayerEconomyStorage.CreateDefaultPayload();
|
||||
SyncToPlayerData();
|
||||
SaveNow();
|
||||
loadedFromSave = true;
|
||||
hasAnyRecoverableLocalState = true;
|
||||
NotifyEconomyChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -217,6 +228,13 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
|
||||
SaveNow();
|
||||
}
|
||||
|
||||
public void SetMaterial(int value)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
payload.material = Mathf.Max(0, value);
|
||||
SaveNow();
|
||||
}
|
||||
|
||||
public void SaveNow()
|
||||
{
|
||||
InitializeIfNeededForSave();
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class PlayerEconomyStorage
|
||||
private const string MainFileName = ".eco.dat";
|
||||
private const string BackupFileName = ".eco.bak";
|
||||
private const string TempFileName = ".eco.tmp";
|
||||
private const string RecoverySlotKey = "player_economy_storage";
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
@@ -53,9 +54,23 @@ public static class PlayerEconomyStorage
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState()
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasEconomyBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
public static bool TrySave(PlayerEconomyPayload payload)
|
||||
{
|
||||
try
|
||||
@@ -77,6 +92,7 @@ public static class PlayerEconomyStorage
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveEconomy(payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -170,6 +186,20 @@ public static class PlayerEconomyStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasAnyVaultFile(string fileName)
|
||||
{
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(candidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(PlayerEconomyPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
private PlayerExperiencePayload payload;
|
||||
private bool initialized;
|
||||
private bool loadedFromSave;
|
||||
private bool hasAnyRecoverableLocalState;
|
||||
private Player_SO boundPlayerData;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
|
||||
@@ -75,6 +76,8 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
hasAnyRecoverableLocalState = SecureSaveVault.HasAnyRecoverableState("player_experience", "runtime", GetLegacySavePath())
|
||||
|| PlayerProgressBackupService.HasPlayerExperienceBackup();
|
||||
loadedFromSave = SecureSaveVault.TryLoadJson("player_experience", "runtime", out payload, GetLegacySavePath());
|
||||
if (payload == null)
|
||||
{
|
||||
@@ -91,6 +94,8 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
hasAnyRecoverableLocalState = hasAnyRecoverableLocalState || loadedFromSave;
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@@ -104,11 +109,16 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
InitializeIfNeeded();
|
||||
boundPlayerData = playerData;
|
||||
|
||||
if (!loadedFromSave)
|
||||
if (!loadedFromSave && !hasAnyRecoverableLocalState)
|
||||
{
|
||||
payload.playerExp = Mathf.Max(0, playerData.player_currentEXP);
|
||||
// 首次启动且无任何可恢复存档:用硬编码默认值(0)建立初始存档,
|
||||
// 不读取 Player_SO 的烘焙经验值,避免打包测试数据污染玩家进度。
|
||||
payload = CreateDefaultPayload();
|
||||
SyncToPlayerData();
|
||||
SaveNow();
|
||||
loadedFromSave = true;
|
||||
hasAnyRecoverableLocalState = true;
|
||||
NotifyExperienceChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,6 +132,13 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
return payload.playerExp;
|
||||
}
|
||||
|
||||
public void SetExperience(int amount)
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
payload.playerExp = Mathf.Max(0, amount);
|
||||
SaveNow();
|
||||
}
|
||||
|
||||
public void AddExperience(int amount)
|
||||
{
|
||||
if (amount == 0)
|
||||
@@ -181,6 +198,16 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
|
||||
NotifyExperienceChanged();
|
||||
}
|
||||
|
||||
public void ClearPersistentState()
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
payload = CreateDefaultPayload();
|
||||
SecureSaveVault.Delete("player_experience", "runtime", GetLegacySavePath());
|
||||
PlayerProgressBackupService.ClearPlayerExperienceBackup();
|
||||
SyncToPlayerData();
|
||||
NotifyExperienceChanged();
|
||||
}
|
||||
|
||||
private void SyncToPlayerData()
|
||||
{
|
||||
if (boundPlayerData == null || payload == null)
|
||||
|
||||
@@ -10,6 +10,7 @@ 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;
|
||||
@@ -27,6 +28,62 @@ public static class PlayerProgressBackupService
|
||||
private static bool s_cacheLoaded;
|
||||
private static bool s_isWriting;
|
||||
|
||||
public static bool HasExistingBackupFile()
|
||||
{
|
||||
IReadOnlyList<string> 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 HasStoreOwnershipBackup()
|
||||
{
|
||||
PlayerProgressBackupBundle bundle;
|
||||
return TryLoadBundle(out bundle) && bundle != null && bundle.storeOwnership != null;
|
||||
}
|
||||
|
||||
public static bool TryRestoreEconomy(out PlayerEconomyPayload payload)
|
||||
{
|
||||
payload = null;
|
||||
@@ -59,7 +116,7 @@ public static class PlayerProgressBackupService
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bundle.playerExperience <= 0)
|
||||
if (!bundle.hasPlayerExperience && bundle.playerExperience <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -70,7 +127,11 @@ public static class PlayerProgressBackupService
|
||||
|
||||
public static void SavePlayerExperience(int experience)
|
||||
{
|
||||
UpdateBundle(bundle => bundle.playerExperience = Mathf.Max(0, experience));
|
||||
UpdateBundle(bundle =>
|
||||
{
|
||||
bundle.hasPlayerExperience = true;
|
||||
bundle.playerExperience = Mathf.Max(0, experience);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool TryRestoreRks(out float rks)
|
||||
@@ -229,6 +290,59 @@ public static class PlayerProgressBackupService
|
||||
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<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);
|
||||
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<PlayerProgressBackupBundle> mutator)
|
||||
{
|
||||
if (mutator == null || s_isWriting)
|
||||
|
||||
@@ -87,6 +87,7 @@ public static class PlayerRksService
|
||||
loaded = true;
|
||||
bestOverallRks = 0f;
|
||||
SecureSaveVault.Delete(SaveCategory, SaveKey);
|
||||
PlayerProgressBackupService.ClearRksBackup();
|
||||
SyncPlayerSo(player);
|
||||
OnRksChanged?.Invoke(bestOverallRks);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,11 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
EnsureInstance().ClearPersistentStateInternal();
|
||||
}
|
||||
|
||||
public static void ResetToDefaultLevelOneSkill()
|
||||
{
|
||||
EnsureInstance().ResetToDefaultLevelOneSkillInternal();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
@@ -423,6 +428,35 @@ public sealed class PlayerSkillService : MonoBehaviour
|
||||
{
|
||||
saveData = new PlayerSkillSaveData();
|
||||
SecureSaveVault.Delete(SaveCategory, SaveKey);
|
||||
PlayerProgressBackupService.ClearPlayerSkillBackup();
|
||||
ApplySceneBindings();
|
||||
}
|
||||
|
||||
private void ResetToDefaultLevelOneSkillInternal()
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
ResolveSkillAssetIfNeeded();
|
||||
|
||||
saveData = saveData ?? new PlayerSkillSaveData();
|
||||
saveData.postMatchRewardCounter = 0;
|
||||
saveData.skillSwitchCooldownRemainingMatches = 0;
|
||||
|
||||
int defaultIndex = -1;
|
||||
if (registeredSkillAsset != null && registeredSkillAsset.skills != null && registeredSkillAsset.skills.Count > 0)
|
||||
{
|
||||
defaultIndex = 0;
|
||||
for (int i = 0; i < registeredSkillAsset.skills.Count; i++)
|
||||
{
|
||||
userLevel_skills_SO.UserLevelSkillEntry entry = registeredSkillAsset.skills[i];
|
||||
if (entry != null)
|
||||
{
|
||||
entry.isEnabled = i == defaultIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveData.selectedSkillIndex = defaultIndex;
|
||||
SaveNow();
|
||||
ApplySceneBindings();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ public static class RecentPlayHistoryStore
|
||||
private const string Category = "recent_play_history";
|
||||
private const string Key = "runs_v1";
|
||||
private const int MaxRecordCount = 100;
|
||||
private const string RecoverySlotKey = "recent_play_history_runs_v1";
|
||||
|
||||
public static IReadOnlyList<RecentPlayRecord> GetRecords()
|
||||
{
|
||||
@@ -32,11 +33,13 @@ public static class RecentPlayHistoryStore
|
||||
}
|
||||
|
||||
SecureSaveVault.SaveJson(Category, Key, payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
}
|
||||
|
||||
public static void Clear()
|
||||
{
|
||||
SecureSaveVault.Delete(Category, Key);
|
||||
LocalRecoveryMirror.DeleteSlot(RecoverySlotKey);
|
||||
}
|
||||
|
||||
private static RecentPlayHistoryPayload LoadPayload()
|
||||
@@ -44,7 +47,14 @@ public static class RecentPlayHistoryStore
|
||||
RecentPlayHistoryPayload payload;
|
||||
if (!SecureSaveVault.TryLoadJson(Category, Key, out payload) || payload == null)
|
||||
{
|
||||
payload = new RecentPlayHistoryPayload();
|
||||
if (!LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) || payload == null)
|
||||
{
|
||||
payload = new RecentPlayHistoryPayload();
|
||||
}
|
||||
else
|
||||
{
|
||||
SecureSaveVault.SaveJson(Category, Key, payload);
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.records == null)
|
||||
|
||||
@@ -19,6 +19,7 @@ public static class SecureSaveVault
|
||||
{
|
||||
private const string SecretSeed = "ban_total.secure_save_v2";
|
||||
private const string VaultDirectoryName = ".cache_bridge";
|
||||
private const string RecoveryDirectoryName = ".save_recovery";
|
||||
private static bool s_dpapiInitialized;
|
||||
private static bool s_dpapiSupported;
|
||||
private static MethodInfo s_dpapiProtectMethod;
|
||||
@@ -30,6 +31,11 @@ public static class SecureSaveVault
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), VaultDirectoryName); }
|
||||
}
|
||||
|
||||
private static string RecoveryDirectoryPath
|
||||
{
|
||||
get { return Path.Combine(SaveIdentityUtility.GetCanonicalPersistentRoot(), RecoveryDirectoryName); }
|
||||
}
|
||||
|
||||
public static bool SaveJson<T>(string category, string key, T data, string legacyPlainPath = null)
|
||||
{
|
||||
if (!typeof(T).IsValueType && (object)data == null)
|
||||
@@ -103,6 +109,7 @@ public static class SecureSaveVault
|
||||
File.Copy(tempPath, mainPath, true);
|
||||
TryHidePath(mainPath);
|
||||
File.Delete(tempPath);
|
||||
TrySaveRecoveryCopy(category, key, json);
|
||||
DeleteLegacyPlainFile(legacyPlainPath);
|
||||
return true;
|
||||
}
|
||||
@@ -126,6 +133,12 @@ public static class SecureSaveVault
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryLoadFromRecoveryCopies(category, key, out json))
|
||||
{
|
||||
SaveRawJson(category, key, json, legacyPlainPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(legacyPlainPath) && File.Exists(legacyPlainPath))
|
||||
{
|
||||
try
|
||||
@@ -236,6 +249,47 @@ public static class SecureSaveVault
|
||||
return Directory.GetFiles(categoryDirectory, "*.dat", SearchOption.TopDirectoryOnly).Length;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState(string category, string key, string legacyPlainPath = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants(legacyPlainPath);
|
||||
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
|
||||
for (int rootIndex = 0; rootIndex < roots.Count; rootIndex++)
|
||||
{
|
||||
string root = roots[rootIndex];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int variantIndex = 0; variantIndex < identifierVariants.Count; variantIndex++)
|
||||
{
|
||||
string applicationIdentifier = identifierVariants[variantIndex];
|
||||
if (File.Exists(GetFilePathForRoot(root, category, key, ".dat", applicationIdentifier))
|
||||
|| File.Exists(GetFilePathForRoot(root, category, key, ".bak", applicationIdentifier)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IReadOnlyList<string> recoveryCandidates = GetRecoveryFilePathVariants(category, key);
|
||||
for (int i = 0; i < recoveryCandidates.Count; i++)
|
||||
{
|
||||
string path = recoveryCandidates[i];
|
||||
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(legacyPlainPath) && File.Exists(legacyPlainPath);
|
||||
}
|
||||
|
||||
private static bool TryReadEncryptedFile(string category, string key, string filePath, out string json)
|
||||
{
|
||||
json = null;
|
||||
@@ -616,6 +670,117 @@ public static class SecureSaveVault
|
||||
return Path.Combine(categoryDirectory, "." + safeKey + extension);
|
||||
}
|
||||
|
||||
private static bool TryLoadFromRecoveryCopies(string category, string key, out string json)
|
||||
{
|
||||
json = null;
|
||||
IReadOnlyList<string> candidates = GetRecoveryFilePathVariants(category, key);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
string path = candidates[i];
|
||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string loaded = File.ReadAllText(path, Encoding.UTF8);
|
||||
if (string.IsNullOrWhiteSpace(loaded))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
json = loaded;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[SecureSaveVault] Recovery read failed ({path}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void TrySaveRecoveryCopy(string category, string key, string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key) || json == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(RecoveryDirectoryPath);
|
||||
string recoveryPath = GetRecoveryFilePath(category, key);
|
||||
string backupPath = recoveryPath + ".bak";
|
||||
string tempPath = recoveryPath + ".tmp";
|
||||
|
||||
File.WriteAllText(tempPath, json, Encoding.UTF8);
|
||||
if (File.Exists(recoveryPath))
|
||||
{
|
||||
File.Copy(recoveryPath, backupPath, true);
|
||||
}
|
||||
|
||||
File.Copy(tempPath, recoveryPath, true);
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[SecureSaveVault] Recovery mirror save failed ({category}/{key}): {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetRecoveryFilePath(string category, string key)
|
||||
{
|
||||
string safeName = ShortHash("recovery|" + category + "|" + key);
|
||||
return Path.Combine(RecoveryDirectoryPath, safeName + ".json");
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetRecoveryFilePathVariants(string category, string key)
|
||||
{
|
||||
var result = new List<string>();
|
||||
if (string.IsNullOrWhiteSpace(category) || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
string safeName = ShortHash("recovery|" + category + "|" + key) + ".json";
|
||||
string safeBackupName = safeName + ".bak";
|
||||
IReadOnlyList<string> roots = SaveIdentityUtility.GetPersistentRootVariants();
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
string root = roots[i];
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
AddDistinctPath(result, Path.Combine(root, RecoveryDirectoryName, safeName));
|
||||
AddDistinctPath(result, Path.Combine(root, RecoveryDirectoryName, safeBackupName));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void AddDistinctPath(List<string> target, string value)
|
||||
{
|
||||
if (target == null || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < target.Count; i++)
|
||||
{
|
||||
if (string.Equals(target[i], value, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
target.Add(value);
|
||||
}
|
||||
|
||||
private static void DeleteLegacyPlainFile(string legacyPlainPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(legacyPlainPath) || !File.Exists(legacyPlainPath))
|
||||
|
||||
@@ -11,6 +11,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
|
||||
public static StoreOwnershipLedger Instance { get; private set; }
|
||||
|
||||
public event Action<int> OnOwnershipChanged;
|
||||
|
||||
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
|
||||
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
|
||||
private bool initialized;
|
||||
@@ -72,11 +74,12 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
initialized = true;
|
||||
StoreOwnershipPayload payload;
|
||||
bool loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
|
||||
bool hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
|
||||
RebuildFromPayload(payload);
|
||||
LoadStoreItems();
|
||||
|
||||
bool recoveredFromMirrors = false;
|
||||
if (!loadedFromSave)
|
||||
if (!loadedFromSave && !hasAnyRecoverableLocalState)
|
||||
{
|
||||
recoveredFromMirrors = SeedFromCurrentMirrorFlags();
|
||||
}
|
||||
@@ -149,6 +152,7 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
ApplyEntryToItem(itemSO, entry);
|
||||
GlobalAchievementService.EnsureInstance().RefreshDerivedMetrics();
|
||||
SaveNow();
|
||||
NotifyOwnershipChanged(itemSO.itemID);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -187,6 +191,7 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
SyncAllMirrorFlags();
|
||||
GlobalAchievementService.EnsureInstance().RefreshDerivedMetrics();
|
||||
SaveNow();
|
||||
NotifyOwnershipChanged(itemSO.itemID);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -212,6 +217,14 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
StoreOwnershipStorage.TrySave(CreatePayload());
|
||||
}
|
||||
|
||||
public void ClearPersistentState()
|
||||
{
|
||||
InitializeIfNeeded();
|
||||
entriesByItemId.Clear();
|
||||
SyncAllMirrorFlags();
|
||||
StoreOwnershipStorage.TrySave(CreatePayload());
|
||||
}
|
||||
|
||||
private void RebuildFromPayload(StoreOwnershipPayload payload)
|
||||
{
|
||||
entriesByItemId.Clear();
|
||||
@@ -770,4 +783,14 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void NotifyOwnershipChanged(int itemId)
|
||||
{
|
||||
if (itemId <= 0 || OnOwnershipChanged == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
OnOwnershipChanged(itemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public static class StoreOwnershipStorage
|
||||
private const string MainFileName = ".own.dat";
|
||||
private const string BackupFileName = ".own.bak";
|
||||
private const string TempFileName = ".own.tmp";
|
||||
private const string RecoverySlotKey = "store_ownership_storage";
|
||||
|
||||
private static string VaultDirectoryPath
|
||||
{
|
||||
@@ -53,9 +54,23 @@ public static class StoreOwnershipStorage
|
||||
return true;
|
||||
}
|
||||
|
||||
if (LocalRecoveryMirror.TryLoadJson(RecoverySlotKey, out payload) && payload != null)
|
||||
{
|
||||
TrySave(payload);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool HasAnyRecoverableState()
|
||||
{
|
||||
return HasAnyVaultFile(MainFileName)
|
||||
|| HasAnyVaultFile(BackupFileName)
|
||||
|| PlayerProgressBackupService.HasStoreOwnershipBackup()
|
||||
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
|
||||
}
|
||||
|
||||
public static bool TrySave(StoreOwnershipPayload payload)
|
||||
{
|
||||
try
|
||||
@@ -77,6 +92,7 @@ public static class StoreOwnershipStorage
|
||||
TryHidePath(MainFilePath);
|
||||
File.Delete(TempFilePath);
|
||||
PlayerProgressBackupService.SaveStoreOwnership(payload);
|
||||
LocalRecoveryMirror.SaveJson(RecoverySlotKey, payload);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -169,6 +185,20 @@ public static class StoreOwnershipStorage
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool HasAnyVaultFile(string fileName)
|
||||
{
|
||||
var candidates = SaveIdentityUtility.GetVaultFilePathVariants(fileName);
|
||||
for (int i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
if (File.Exists(candidates[i]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildEnvelopeJson(StoreOwnershipPayload payload)
|
||||
{
|
||||
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
Reference in New Issue
Block a user