修了不少东西

This commit is contained in:
FloatGaming
2026-07-16 23:04:59 +08:00
parent 29972d0705
commit 73fe474cc6
134 changed files with 7673 additions and 741 deletions
@@ -313,6 +313,12 @@ public sealed class AllyHeroDeployLedger : MonoBehaviour
return;
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[AllyHeroDeployLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older idol growth data.");
return;
}
SyncAllMirrorFlags();
SyncAllLegacySelectedSlotExpKeys();
AllyHeroDeployLedgerStorage.TrySave(BuildPayload());
@@ -128,20 +128,24 @@ public static class AllyHeroDeployLedgerStorage
byte[] encryptedBytes = Convert.FromBase64String(envelope.payload);
AllyHeroDeployLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
string payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
byte[] plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
string payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<AllyHeroDeployLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -208,25 +212,19 @@ public static class AllyHeroDeployLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
string signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (SHA256 sha = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(signText);
byte[] hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (SHA256 sha = SHA256.Create())
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -234,21 +232,25 @@ public static class AllyHeroDeployLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
string expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
string signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (SHA256 sha = SHA256.Create())
{
byte[] bytes = Encoding.UTF8.GetBytes(signText);
@@ -197,6 +197,12 @@ public sealed class DushMaterialLedger : MonoBehaviour
InitializeIfNeeded();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[DushMaterialLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
return;
}
SyncMirrorCounts();
DushMaterialLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
@@ -138,20 +138,24 @@ public static class DushMaterialLedgerStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
DushMaterialLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<DushMaterialLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -218,25 +222,19 @@ public static class DushMaterialLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -244,21 +242,25 @@ public static class DushMaterialLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -10,6 +10,8 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
private readonly Dictionary<string, int> countsByKey = new Dictionary<string, int>(StringComparer.Ordinal);
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Bootstrap()
@@ -65,11 +67,18 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
}
EquipmentConsumableLedgerPayload payload;
EquipmentConsumableLedgerStorage.TryLoad(out payload);
loadedFromSave = EquipmentConsumableLedgerStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || EquipmentConsumableLedgerStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
initialized = true;
SyncMirrorCounts();
SaveNow();
// If an older save exists but cannot be read right now, do not overwrite it
// with an empty default payload during startup.
if (loadedFromSave || !hasAnyRecoverableLocalState)
{
SaveNow();
}
}
public int GetCount(EquipmentConsumableKind kind)
@@ -126,6 +135,12 @@ public sealed class EquipmentConsumableLedger : MonoBehaviour
InitializeIfNeeded();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[EquipmentConsumableLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
return;
}
SyncMirrorCounts();
EquipmentConsumableLedgerStorage.TrySave(BuildPayload());
}
@@ -52,6 +52,7 @@ public static class EquipmentConsumableLedgerStorage
{
return HasAnyVaultFile(MainFileName)
|| HasAnyVaultFile(BackupFileName)
|| PlayerProgressBackupService.HasEquipmentConsumableBackup()
|| LocalRecoveryMirror.HasSlotData(RecoverySlotKey);
}
@@ -136,20 +137,24 @@ public static class EquipmentConsumableLedgerStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
EquipmentConsumableLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<EquipmentConsumableLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -202,25 +207,19 @@ public static class EquipmentConsumableLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -228,21 +227,25 @@ public static class EquipmentConsumableLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -295,6 +295,13 @@ public sealed class ExpBottleLedger : MonoBehaviour
public void SaveNow()
{
InitializeIfNeededForSave();
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[ExpBottleLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older material data.");
return;
}
SyncMirrorCounts();
ExpBottleLedgerStorage.TrySave(BuildPayload());
SyncToPlayerData();
@@ -138,20 +138,24 @@ public static class ExpBottleLedgerStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
ExpBottleLedgerPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<ExpBottleLedgerPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -218,25 +222,19 @@ public static class ExpBottleLedgerStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -244,21 +242,25 @@ public static class ExpBottleLedgerStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -48,11 +48,99 @@ public static class FirstRunFactoryResetService
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
@@ -238,6 +238,13 @@ public sealed class PlayerEconomyLedger : MonoBehaviour
public void SaveNow()
{
InitializeIfNeededForSave();
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[PlayerEconomyLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older economy data.");
return;
}
PlayerEconomyStorage.TrySave(payload);
SyncToPlayerData();
GlobalAchievementService.EnsureInstance().ReportCurrentCoins(payload.coins);
@@ -138,21 +138,25 @@ public static class PlayerEconomyStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
PlayerEconomyPayload loadedPayload = null;
for (int i = 0; i < identifierVariants.Count; i++)
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<PlayerEconomyPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -219,25 +223,19 @@ public static class PlayerEconomyStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -245,21 +243,25 @@ public static class PlayerEconomyStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
@@ -191,6 +191,12 @@ public sealed class PlayerExperienceLedger : MonoBehaviour
payload = CreateDefaultPayload();
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[PlayerExperienceLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older player experience data.");
return;
}
payload.lastUpdatedUtcTicks = DateTime.UtcNow.Ticks;
SecureSaveVault.SaveJson("player_experience", "runtime", payload, GetLegacySavePath());
PlayerProgressBackupService.SavePlayerExperience(payload.playerExp);
@@ -78,6 +78,12 @@ public static class PlayerProgressBackupService
return TryLoadBundle(out bundle) && bundle != null && bundle.dushMaterial != null;
}
public static bool HasEquipmentConsumableBackup()
{
PlayerProgressBackupBundle bundle;
return TryLoadBundle(out bundle) && bundle != null && bundle.equipmentConsumable != null;
}
public static bool HasStoreOwnershipBackup()
{
PlayerProgressBackupBundle bundle;
@@ -301,7 +301,7 @@ public sealed class PlayerSkillService : MonoBehaviour
if (grantedCoins > 0)
{
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
grantedEntries.Add(gItemGet.Create("\u91d1\u5e01", null, grantedCoins, new Color32(240, 196, 74, 255)));
grantedEntries.Add(gItemGet.Create("算力", null, grantedCoins, new Color32(240, 196, 74, 255)));
}
}
@@ -376,7 +376,7 @@ public sealed class PlayerSkillService : MonoBehaviour
int grantedCoins = purchaseAmount * 4;
PlayerEconomyLedger.EnsureInstance().AddCoins(grantedCoins);
rewardEntry = gItemGet.Create("\u91d1\u5e01", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
rewardEntry = gItemGet.Create("算力", item.itemIcon, grantedCoins, new Color32(240, 196, 74, 255));
return true;
}
@@ -31,11 +31,69 @@ public static class SaveIdentityUtility
"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<string> s_cachedApplicationIdentifierVariants;
private static IReadOnlyList<string> 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<string> GetDeviceBindingVariants()
{
if (s_cachedDeviceBindingVariants != null)
{
return s_cachedDeviceBindingVariants;
}
var result = new List<string>();
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)
@@ -50,6 +108,11 @@ public static class SaveIdentityUtility
public static IReadOnlyList<string> GetApplicationIdentifierVariants()
{
if (s_cachedApplicationIdentifierVariants != null)
{
return s_cachedApplicationIdentifierVariants;
}
var result = new List<string>();
AddDistinct(result, StableApplicationIdentifier);
AddDistinct(result, Application.identifier);
@@ -74,7 +137,8 @@ public static class SaveIdentityUtility
}
}
return result;
s_cachedApplicationIdentifierVariants = result;
return s_cachedApplicationIdentifierVariants;
}
public static IReadOnlyList<string> GetPersistentRootVariants(string extraLegacyPath = null)
+54 -47
View File
@@ -373,12 +373,7 @@ public static class SecureSaveVault
private static string ComputeSignature(string category, string payloadBase64)
{
string signText = payloadBase64 + "|" + category + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
return Convert.ToBase64String(hash);
}
return ComputeSignature(category, payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] ProtectBytes(string category, string key, byte[] plainBytes)
@@ -394,7 +389,7 @@ public static class SecureSaveVault
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier());
aes.Key = BuildAesKey(category, key, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
aes.GenerateIV();
using (var encryptor = aes.CreateEncryptor())
{
@@ -419,37 +414,41 @@ public static class SecureSaveVault
}
#endif
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
using (var aes = Aes.Create())
for (int dv = 0; dv < deviceBindingVariants.Count; dv++)
{
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, identifierVariants[i]);
int ivLength = aes.BlockSize / 8;
if (protectedBytes == null || protectedBytes.Length <= ivLength)
using (var aes = Aes.Create())
{
return false;
}
byte[] iv = new byte[ivLength];
byte[] cipher = new byte[protectedBytes.Length - ivLength];
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
aes.IV = iv;
try
{
using (var decryptor = aes.CreateDecryptor())
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = BuildAesKey(category, key, identifierVariants[i], deviceBindingVariants[dv]);
int ivLength = aes.BlockSize / 8;
if (protectedBytes == null || protectedBytes.Length <= ivLength)
{
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
if (plainBytes != null)
return false;
}
byte[] iv = new byte[ivLength];
byte[] cipher = new byte[protectedBytes.Length - ivLength];
Buffer.BlockCopy(protectedBytes, 0, iv, 0, ivLength);
Buffer.BlockCopy(protectedBytes, ivLength, cipher, 0, cipher.Length);
aes.IV = iv;
try
{
using (var decryptor = aes.CreateDecryptor())
{
return true;
plainBytes = decryptor.TransformFinalBlock(cipher, 0, cipher.Length);
if (plainBytes != null)
{
return true;
}
}
}
}
catch
{
catch
{
}
}
}
}
@@ -476,7 +475,7 @@ public static class SecureSaveVault
try
{
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier()), s_dpapiCurrentUserScope }) as byte[];
protectedBytes = s_dpapiProtectMethod.Invoke(null, new object[] { plainBytes, BuildEntropy(category, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding()), s_dpapiCurrentUserScope }) as byte[];
return protectedBytes != null && protectedBytes.Length > 0;
}
catch (Exception ex)
@@ -496,18 +495,22 @@ public static class SecureSaveVault
}
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i]), s_dpapiCurrentUserScope }) as byte[];
if (plainBytes != null && plainBytes.Length > 0)
try
{
plainBytes = s_dpapiUnprotectMethod.Invoke(null, new object[] { protectedBytes, BuildEntropy(category, identifierVariants[i], deviceBindingVariants[d]), s_dpapiCurrentUserScope }) as byte[];
if (plainBytes != null && plainBytes.Length > 0)
{
return true;
}
}
catch
{
return true;
}
}
catch
{
}
}
@@ -560,20 +563,20 @@ public static class SecureSaveVault
}
#endif
private static byte[] BuildEntropy(string category, string applicationIdentifier)
private static byte[] BuildEntropy(string category, string applicationIdentifier, string deviceBinding)
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category;
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category;
using (var sha = SHA256.Create())
{
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier)
private static byte[] BuildAesKey(string category, string key, string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
string seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed + "|" + category + "|" + key;
string seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed + "|" + category + "|" + key;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -635,21 +638,25 @@ public static class SecureSaveVault
private static bool TryValidateSignature(string category, string payloadBase64, string signature)
{
IReadOnlyList<string> identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
IReadOnlyList<string> deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
string expectedSignature = ComputeSignature(category, payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string category, string payloadBase64, string applicationIdentifier, string deviceBinding)
{
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
string signText = payloadBase64 + "|" + category + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(signText));
@@ -97,13 +97,13 @@ public static class StoreExpBottlePurchaseService
if (!HasEnoughCurrency(primaryCost.currencyType, totalCost))
{
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
return false;
}
if (!TrySpendCurrency(primaryCost.currencyType, totalCost))
{
failureMessage = LocalizationService.Get("store.error.insufficient_currency", "货币不足");
failureMessage = GetInsufficientCurrencyMessage(primaryCost.currencyType);
return false;
}
@@ -224,7 +224,7 @@ public static class StoreExpBottlePurchaseService
var builder = new StringBuilder();
builder.Append("[StorePurchase] 已成功购买:");
builder.Append(itemSO != null ? itemSO.itemName : LocalizationService.Get("item.unknown.store_item", "Unknown Item"));
builder.Append(" | 花费Coins=");
builder.Append(" | 花费算力=");
builder.Append(totalCost);
builder.Append(" | 发放数量=");
builder.Append(grantedCount);
@@ -341,6 +341,11 @@ public static class StoreExpBottlePurchaseService
|| currencyType == storeItemSO.CurrencyType.material;
}
private static string GetInsufficientCurrencyMessage(storeItemSO.CurrencyType currencyType)
{
return currencyType == storeItemSO.CurrencyType.material ? "记忆碎片不足" : "算力不足";
}
private static bool HasEnoughCurrency(storeItemSO.CurrencyType currencyType, int amount)
{
switch (currencyType)
@@ -16,6 +16,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
private readonly Dictionary<int, StoreOwnershipEntry> entriesByItemId = new Dictionary<int, StoreOwnershipEntry>();
private readonly List<storeItemSO> cachedStoreItems = new List<storeItemSO>();
private bool initialized;
private bool loadedFromSave;
private bool hasAnyRecoverableLocalState;
private int lastSaveFrame = -1;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
@@ -73,8 +75,8 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
initialized = true;
StoreOwnershipPayload payload;
bool loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
bool hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
loadedFromSave = StoreOwnershipStorage.TryLoad(out payload);
hasAnyRecoverableLocalState = loadedFromSave || StoreOwnershipStorage.HasAnyRecoverableState();
RebuildFromPayload(payload);
LoadStoreItems();
@@ -208,6 +210,12 @@ public sealed class StoreOwnershipLedger : MonoBehaviour
return;
}
if (!loadedFromSave && hasAnyRecoverableLocalState)
{
Debug.LogWarning("[StoreOwnershipLedger] Recoverable save state exists but could not be loaded; skip saving to avoid overwriting older store ownership data.");
return;
}
if (Application.isPlaying && lastSaveFrame == Time.frameCount)
{
return;
@@ -138,20 +138,24 @@ public static class StoreOwnershipStorage
var encryptedBytes = Convert.FromBase64String(envelope.payload);
StoreOwnershipPayload loadedPayload = null;
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
for (int i = 0; i < identifierVariants.Count; i++)
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count && loadedPayload == null; i++)
{
try
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
if (loadedPayload != null)
try
{
var plainBytes = XorTransform(encryptedBytes, BuildKeyBytes(identifierVariants[i], deviceBindingVariants[d]));
var payloadJson = Encoding.UTF8.GetString(plainBytes);
loadedPayload = JsonUtility.FromJson<StoreOwnershipPayload>(payloadJson);
if (loadedPayload != null)
{
break;
}
}
catch
{
break;
}
}
catch
{
}
}
@@ -218,25 +222,19 @@ public static class StoreOwnershipStorage
private static string ComputeSignature(string payloadBase64)
{
var signText = payloadBase64 + "|" + SaveIdentityUtility.GetPrimaryApplicationIdentifier() + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);
var hash = sha.ComputeHash(bytes);
return Convert.ToBase64String(hash);
}
return ComputeSignature(payloadBase64, SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes()
{
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier());
return BuildKeyBytes(SaveIdentityUtility.GetPrimaryApplicationIdentifier(), SaveIdentityUtility.GetPrimaryDeviceBinding());
}
private static byte[] BuildKeyBytes(string applicationIdentifier)
private static byte[] BuildKeyBytes(string applicationIdentifier, string deviceBinding)
{
using (var sha = SHA256.Create())
{
var seed = applicationIdentifier + "|" + SystemInfo.deviceUniqueIdentifier + "|" + SecretSeed;
var seed = applicationIdentifier + "|" + deviceBinding + "|" + SecretSeed;
return sha.ComputeHash(Encoding.UTF8.GetBytes(seed));
}
}
@@ -244,21 +242,25 @@ public static class StoreOwnershipStorage
private static bool TryValidateSignature(string payloadBase64, string signature)
{
var identifierVariants = SaveIdentityUtility.GetApplicationIdentifierVariants();
var deviceBindingVariants = SaveIdentityUtility.GetDeviceBindingVariants();
for (int i = 0; i < identifierVariants.Count; i++)
{
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
for (int d = 0; d < deviceBindingVariants.Count; d++)
{
return true;
var expectedSignature = ComputeSignature(payloadBase64, identifierVariants[i], deviceBindingVariants[d]);
if (string.Equals(expectedSignature, signature, StringComparison.Ordinal))
{
return true;
}
}
}
return false;
}
private static string ComputeSignature(string payloadBase64, string applicationIdentifier)
private static string ComputeSignature(string payloadBase64, string applicationIdentifier, string deviceBinding)
{
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + SystemInfo.deviceUniqueIdentifier;
var signText = payloadBase64 + "|" + applicationIdentifier + "|" + SecretSeed + "|" + deviceBinding;
using (var sha = SHA256.Create())
{
var bytes = Encoding.UTF8.GetBytes(signText);