181 lines
6.0 KiB
C#
181 lines
6.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using Cysharp.Threading.Tasks;
|
|
using Newtonsoft.Json.Linq;
|
|
using UnityEngine;
|
|
|
|
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) { Debug.LogException(e); }
|
|
}
|
|
|
|
private static async UniTaskVoid ScheduleWrite()
|
|
{
|
|
_debounceCts?.Cancel();
|
|
_debounceCts = new CancellationTokenSource();
|
|
var token = _debounceCts.Token;
|
|
|
|
try
|
|
{
|
|
var delay = (int)(ShrinkDataSaverSettings.Instance.settingsWriteDebounceSeconds * 1000);
|
|
await UniTask.Delay(delay, cancellationToken: token);
|
|
await SaveAsync(token);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception e) { Debug.LogException(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))
|
|
{
|
|
Debug.LogWarning($"[ShrinkDataSaver] Settings 主文件损坏或缺失,已从备份恢复:{candidatePath}");
|
|
await _storage.WriteAsync(primaryPath, bytes, ct);
|
|
}
|
|
|
|
return loaded;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
lastError = ex;
|
|
Debug.LogWarning($"[ShrinkDataSaver] 读取 Settings 副本失败:{candidatePath} / {ex.Message}");
|
|
}
|
|
}
|
|
|
|
if (lastError != null)
|
|
{
|
|
Debug.LogWarning("[ShrinkDataSaver] 所有 Settings 副本均不可用,已回退为空设置。");
|
|
}
|
|
|
|
return new SettingsData();
|
|
}
|
|
|
|
private static IEnumerable<string> EnumerateCandidatePaths(string primaryPath)
|
|
{
|
|
yield return primaryPath;
|
|
yield return primaryPath + BackupPrimarySuffix;
|
|
yield return primaryPath + BackupSecondarySuffix;
|
|
}
|
|
}
|
|
}
|