客户端rsa,冗余清理,bug修复,安卓build问题

This commit is contained in:
FloatGaming
2026-07-14 01:43:49 +08:00
parent fd22501f71
commit f8985d91d2
682 changed files with 4595 additions and 11216 deletions
@@ -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;