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(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(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(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 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 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 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 GetVariantPaths(string slotKey) { var result = new List(); string fileName = BuildFileName(slotKey); string backupFileName = fileName + ".bak"; IReadOnlyList 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 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); } }