487 lines
16 KiB
C#
487 lines
16 KiB
C#
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<string> 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",
|
|
"noteSpeedMultiplierDefaultVersion",
|
|
"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]}");
|
|
}
|
|
PlayerPrefs.DeleteKey("KeyBinding_yellow_secondary");
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|