using System; using System.Collections.Generic; using System.IO; using UnityEngine; public static class SaveIdentityUtility { public const string StableCompanyName = "FloatGamingStudio"; public const string StableProductName = "Bansonic"; public const string StableApplicationIdentifier = "com.FloatGamingStudio.Bansonic"; private static readonly string[] LegacyProductAliases = { "Bansonic", "ban_total", "EaseOut_Main_FreshNew" }; private static readonly string[] LegacyCompanyAliases = { "FloatGamingStudio", "DefaultCompany" }; private static readonly string[] LegacyIdentifierAliases = { "com.DefaultCompany.Bansonic", "com.FloatGamingStudio.ban_total", "com.DefaultCompany.ban_total", "com.FloatGamingStudio.EaseOut_Main_FreshNew", "com.DefaultCompany.EaseOut_Main_FreshNew" }; // 设备无关绑定令牌:新版存档的加密/签名统一改用它,取代 SystemInfo.deviceUniqueIdentifier。 // 根因修复——deviceUniqueIdentifier 在安卓更新/换签名/换渠道后会变化,导致旧密钥永远解不出存档。 // 用固定令牌后,更新或换机都不再影响解密。读取时仍会尝试真实设备 ID 变体以兼容本机旧存档。 public const string DeviceIndependentBindingToken = "device_independent_binding_v1"; // 【性能】这些值在进程生命周期内恒定:SystemInfo.deviceUniqueIdentifier、Application.identifier/ // companyName/productName 在安卓上开销较大(尤其 deviceUniqueIdentifier 涉及系统调用与哈希)。 // 存档每次读取/验签都会取用它们,若每次重算并分配 List,会在 UI 大量刷新(如 idols 角色切换、 // 场景切换批量重建)时造成明显卡顿。这里缓存一次,之后直接复用,行为完全不变。 private static IReadOnlyList s_cachedApplicationIdentifierVariants; private static IReadOnlyList s_cachedDeviceBindingVariants; private static string s_cachedDeviceUniqueIdentifier; private static bool s_deviceUniqueIdentifierResolved; public static string GetPrimaryApplicationIdentifier() { return StableApplicationIdentifier; } // 写入存档时使用的设备绑定:固定为设备无关令牌。 public static string GetPrimaryDeviceBinding() { return DeviceIndependentBindingToken; } // 读取/验签时尝试的设备绑定变体,按命中概率排序: // 1) 设备无关令牌(新版写入所用,最常命中) // 2) 当前设备真实 deviceUniqueIdentifier(兼容本机旧版写入的设备绑定存档,首次读到后会被重写为设备无关,实现无缝迁移) // 结果缓存,避免每次读取都触发一次昂贵的 deviceUniqueIdentifier 查询与 List 分配。 public static IReadOnlyList GetDeviceBindingVariants() { if (s_cachedDeviceBindingVariants != null) { return s_cachedDeviceBindingVariants; } var result = new List(); AddDistinct(result, DeviceIndependentBindingToken); AddDistinct(result, SafeDeviceUniqueIdentifier()); s_cachedDeviceBindingVariants = result; return s_cachedDeviceBindingVariants; } private static string SafeDeviceUniqueIdentifier() { if (s_deviceUniqueIdentifierResolved) { return s_cachedDeviceUniqueIdentifier; } try { s_cachedDeviceUniqueIdentifier = SystemInfo.deviceUniqueIdentifier; } catch (Exception) { s_cachedDeviceUniqueIdentifier = null; } s_deviceUniqueIdentifierResolved = true; return s_cachedDeviceUniqueIdentifier; } public static string GetCanonicalPersistentRoot() { if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer) { string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string localLow = Path.GetFullPath(Path.Combine(localAppData, "..", "LocalLow")); return Path.Combine(localLow, StableCompanyName, StableProductName); } return Application.persistentDataPath; } public static IReadOnlyList GetApplicationIdentifierVariants() { if (s_cachedApplicationIdentifierVariants != null) { return s_cachedApplicationIdentifierVariants; } var result = new List(); AddDistinct(result, StableApplicationIdentifier); AddDistinct(result, Application.identifier); string currentCompany = SafeTrim(Application.companyName); string currentProduct = SafeTrim(Application.productName); if (!string.IsNullOrEmpty(currentCompany) && !string.IsNullOrEmpty(currentProduct)) { AddDistinct(result, $"com.{currentCompany}.{currentProduct}"); } for (int i = 0; i < LegacyIdentifierAliases.Length; i++) { AddDistinct(result, LegacyIdentifierAliases[i]); } for (int companyIndex = 0; companyIndex < LegacyCompanyAliases.Length; companyIndex++) { for (int productIndex = 0; productIndex < LegacyProductAliases.Length; productIndex++) { AddDistinct(result, $"com.{LegacyCompanyAliases[companyIndex]}.{LegacyProductAliases[productIndex]}"); } } s_cachedApplicationIdentifierVariants = result; return s_cachedApplicationIdentifierVariants; } public static IReadOnlyList GetPersistentRootVariants(string extraLegacyPath = null) { var result = new List(); AddDistinct(result, GetCanonicalPersistentRoot()); AddDistinct(result, Application.persistentDataPath); string extraDirectory = string.IsNullOrEmpty(extraLegacyPath) ? null : Path.GetDirectoryName(extraLegacyPath); AddDistinct(result, extraDirectory); if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.WindowsPlayer) { string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); string localLow = Path.GetFullPath(Path.Combine(localAppData, "..", "LocalLow")); string currentCompany = SafeTrim(Application.companyName); string currentProduct = SafeTrim(Application.productName); if (!string.IsNullOrEmpty(currentCompany) && !string.IsNullOrEmpty(currentProduct)) { AddDistinct(result, Path.Combine(localLow, currentCompany, currentProduct)); } for (int companyIndex = 0; companyIndex < LegacyCompanyAliases.Length; companyIndex++) { for (int productIndex = 0; productIndex < LegacyProductAliases.Length; productIndex++) { AddDistinct(result, Path.Combine(localLow, LegacyCompanyAliases[companyIndex], LegacyProductAliases[productIndex])); } } } return result; } public static IReadOnlyList GetVaultFilePathVariants(string fileName) { var result = new List(); if (string.IsNullOrWhiteSpace(fileName)) { return result; } IReadOnlyList roots = GetPersistentRootVariants(); for (int i = 0; i < roots.Count; i++) { string root = roots[i]; if (string.IsNullOrWhiteSpace(root)) { continue; } AddDistinct(result, Path.Combine(root, ".cache_bridge", fileName)); } return result; } private static void AddDistinct(List target, string value) { if (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 string SafeTrim(string value) { return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); } }