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; } // 物理文件兜底探测(最后一道防线): // HasAnyRecoverableLocalState() 依赖各 Storage 的解密/结构判断,若因根目录别名未覆盖、 // 或存档结构异常而误判为"无存档",仍可能走到出厂重置。这里直接扫描磁盘上是否存在 // 任何存档物理文件(不解密、只看存在),只要有就绝不重置,宁可等待其它恢复路径。 if (HasAnyPhysicalSaveFileOnDisk()) { Debug.LogWarning("[FactoryReset] 磁盘上存在存档物理文件但未能读出," + "为避免误删玩家数据,跳过出厂重置并补写初始化标记。"); EnsureInitializedMarker(); return; } Debug.LogWarning("[FactoryReset] 首次运行且无可恢复存档,应用出厂默认设置。"); ApplyFactoryReset(); EnsureInitializedMarker(); } // 扫描所有可能的存档根目录(含新旧包名/公司名别名),只要磁盘上存在任意一个存档物理文件 // 就返回 true。只判存在、不解密——因此不受 deviceUniqueIdentifier 变化影响。 private static bool HasAnyPhysicalSaveFileOnDisk() { try { IReadOnlyList roots = SaveIdentityUtility.GetPersistentRootVariants(); for (int i = 0; i < roots.Count; i++) { string root = roots[i]; if (string.IsNullOrWhiteSpace(root)) { continue; } // 1) 加密存档目录 .cache_bridge:递归找任意 .dat / .bak string vaultDir = Path.Combine(root, ".cache_bridge"); if (DirectoryHasFileWithAnyExtension(vaultDir, new[] { ".dat", ".bak" })) { return true; } // 2) 设备无关明文备份 player_progress.bbackup if (File.Exists(Path.Combine(root, "player_progress.bbackup"))) { return true; } // 3) 恢复镜像目录 .save_recovery:存在任意文件即视为有存档 string recoveryDir = Path.Combine(root, ".save_recovery"); if (DirectoryHasAnyFile(recoveryDir)) { return true; } } } catch (Exception ex) { // 探测失败时采取保守策略:报告"存在存档",宁可跳过重置也不误删。 Debug.LogWarning("[FactoryReset] 物理存档探测异常,保守跳过出厂重置: " + ex.Message); return true; } return false; } private static bool DirectoryHasFileWithAnyExtension(string directory, string[] extensions) { if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) { return false; } for (int i = 0; i < extensions.Length; i++) { string[] matches = Directory.GetFiles(directory, "*" + extensions[i], SearchOption.AllDirectories); if (matches != null && matches.Length > 0) { return true; } } return false; } private static bool DirectoryHasAnyFile(string directory) { if (string.IsNullOrWhiteSpace(directory) || !Directory.Exists(directory)) { return false; } string[] matches = Directory.GetFiles(directory, "*", SearchOption.AllDirectories); return matches != null && matches.Length > 0; } 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(); } }