Files

181 lines
6.0 KiB
C#

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<string, JToken> 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<string, List<Action<object>>> _watchers = new();
public static event Action<string, object> OnChanged;
internal static void Initialize(IStorageProvider storage, string filePath)
{
_storage = storage; _filePath = filePath;
}
public static void Set<T>(string key, T value)
{
_data.Values[key] = JToken.FromObject(value);
FireChanged(key, value);
ScheduleWrite().Forget();
}
public static T Get<T>(string key, T defaultValue = default)
{
if (_data.Values.TryGetValue(key, out var token))
{
try { return token.ToObject<T>(); } catch { /* ignore */ }
}
return defaultValue;
}
public static bool Has(string key) => _data.Values.ContainsKey(key);
public static IReadOnlyDictionary<string, JToken> GetAllRaw() => _data.Values;
public static void Remove(string key)
{
if (_data.Values.Remove(key))
{
FireChanged(key, null);
ScheduleWrite().Forget();
}
}
public static void Watch<T>(string key, Action<T> callback)
{
if (!_watchers.ContainsKey(key)) _watchers[key] = new List<Action<object>>();
_watchers[key].Add(raw => callback((T)Convert.ChangeType(raw, typeof(T))));
}
public static void Unwatch(string key, Action<object> 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<SettingsData> 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<SettingsData>(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<string> EnumerateCandidatePaths(string primaryPath)
{
yield return primaryPath;
yield return primaryPath + BackupPrimarySuffix;
yield return primaryPath + BackupSecondarySuffix;
}
}
}