using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Cysharp.Threading.Tasks; using Newtonsoft.Json.Linq; namespace ShrinkDataSaver { [Serializable] internal class SettingsData { public Dictionary Values = new(); } public static class ShrinkSettings { private const string BackupPrimarySuffix = ".bak1"; private const string BackupSecondarySuffix = ".bak2"; private static SettingsData _data = new(); private static IStorageProvider _storage; private static string _filePath; private static CancellationTokenSource _debounceCts; private static readonly Dictionary>> _watchers = new(); public static event Action OnChanged; internal static void Initialize(IStorageProvider storage, string filePath) { _storage = storage; _filePath = filePath; } public static void Set(string key, T value) { _data.Values[key] = JToken.FromObject(value); FireChanged(key, value); ScheduleWrite().Forget(); } public static T Get(string key, T defaultValue = default) { if (_data.Values.TryGetValue(key, out var token)) { try { return token.ToObject(); } catch { /* ignore */ } } return defaultValue; } public static bool Has(string key) => _data.Values.ContainsKey(key); public static IReadOnlyDictionary GetAllRaw() => _data.Values; public static void Remove(string key) { if (_data.Values.Remove(key)) { FireChanged(key, null); ScheduleWrite().Forget(); } } public static void Watch(string key, Action callback) { if (!_watchers.ContainsKey(key)) _watchers[key] = new List>(); _watchers[key].Add(raw => callback((T)Convert.ChangeType(raw, typeof(T)))); } public static void Unwatch(string key, Action callback) { if (_watchers.TryGetValue(key, out var list)) list.Remove(callback); } public static async UniTask SaveAsync(CancellationToken ct = default) { _debounceCts?.Cancel(); if (_storage == null || string.IsNullOrWhiteSpace(_filePath)) { // 未初始化(独立宿主/单测环境)时跳过持久化,与 LoadAsync 的守卫保持一致 return; } var bytes = DataSerializer.Serialize(_data); await _storage.WriteAsync(_filePath, bytes, ct); } public static async UniTask LoadAsync(CancellationToken ct = default) { if (_storage == null || string.IsNullOrWhiteSpace(_filePath)) { _data = new SettingsData(); return; } var loadedData = await TryLoadWithFallbackAsync(ct); _data = loadedData ?? new SettingsData(); } internal static void ResetForTesting() { _data = new SettingsData(); _debounceCts?.Cancel(); _debounceCts = null; _watchers.Clear(); OnChanged = null; } private static void FireChanged(string key, object value) { OnChanged?.Invoke(key, value); if (_watchers.TryGetValue(key, out var list)) foreach (var cb in list) try { cb(value); } catch (Exception e) { ShrinkDataSaverPlatform.Exception(e); } } private static async UniTaskVoid ScheduleWrite() { _debounceCts?.Cancel(); _debounceCts = new CancellationTokenSource(); var token = _debounceCts.Token; try { var delay = (int)(ShrinkDataSaverPlatform.SettingsWriteDebounceSeconds * 1000); await Task.Delay(delay, token); await SaveAsync(token); } catch (OperationCanceledException) { } catch (Exception e) { ShrinkDataSaverPlatform.Exception(e); } } private static async UniTask TryLoadWithFallbackAsync(CancellationToken ct) { var primaryPath = _filePath; Exception lastError = null; foreach (var candidatePath in EnumerateCandidatePaths(primaryPath)) { if (!await _storage.ExistsAsync(candidatePath, ct)) { continue; } try { var bytes = await _storage.ReadAsync(candidatePath, ct); var loaded = DataSerializer.Deserialize(bytes) ?? new SettingsData(); if (!string.Equals(candidatePath, primaryPath, StringComparison.Ordinal)) { ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] Settings 主文件损坏或缺失,已从备份恢复:{candidatePath}"); await _storage.WriteAsync(primaryPath, bytes, ct); } return loaded; } catch (Exception ex) { lastError = ex; ShrinkDataSaverPlatform.Warning($"[ShrinkDataSaver] 读取 Settings 副本失败:{candidatePath} / {ex.Message}"); } } if (lastError != null) { ShrinkDataSaverPlatform.Warning("[ShrinkDataSaver] 所有 Settings 副本均不可用,已回退为空设置。"); } return new SettingsData(); } private static IEnumerable EnumerateCandidatePaths(string primaryPath) { yield return primaryPath; yield return primaryPath + BackupPrimarySuffix; yield return primaryPath + BackupSecondarySuffix; } } }