mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 22:03:30 +08:00
Merge pull request #180 from GeWuYou/feat/data-unified-settings-repository
Feat/data unified settings repository
This commit is contained in:
commit
dcb9aa229a
20
GFramework.Game.Tests/Data/PersistenceTestUtilities.cs
Normal file
20
GFramework.Game.Tests/Data/PersistenceTestUtilities.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using GFramework.Game.Abstractions.Data;
|
||||||
|
using GFramework.Game.Abstractions.Enums;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Data;
|
||||||
|
|
||||||
|
internal sealed record TestDataLocation(
|
||||||
|
string Key,
|
||||||
|
StorageKinds Kinds = StorageKinds.Local,
|
||||||
|
string? Namespace = null,
|
||||||
|
IReadOnlyDictionary<string, string>? Metadata = null) : IDataLocation;
|
||||||
|
|
||||||
|
internal sealed class TestSaveData : IData
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestSimpleData : IData
|
||||||
|
{
|
||||||
|
public int Value { get; set; }
|
||||||
|
}
|
||||||
95
GFramework.Game.Tests/Data/PersistenceTests.cs
Normal file
95
GFramework.Game.Tests/Data/PersistenceTests.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using System.IO;
|
||||||
|
using GFramework.Game.Abstractions.Data;
|
||||||
|
using GFramework.Game.Data;
|
||||||
|
using GFramework.Game.Serializer;
|
||||||
|
using GFramework.Game.Storage;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Data;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public class PersistenceTests
|
||||||
|
{
|
||||||
|
private static string CreateTempRoot()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), "gframework-persistence", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task FileStorage_PersistsDataAndRejectsIllegalKeys()
|
||||||
|
{
|
||||||
|
var root = CreateTempRoot();
|
||||||
|
using var storage = new FileStorage(root, new JsonSerializer(), ".json");
|
||||||
|
|
||||||
|
var saved = new TestSimpleData { Value = 5 };
|
||||||
|
await storage.WriteAsync("folder/item", saved);
|
||||||
|
|
||||||
|
var loaded = await storage.ReadAsync<TestSimpleData>("folder/item");
|
||||||
|
Assert.That(loaded.Value, Is.EqualTo(saved.Value));
|
||||||
|
|
||||||
|
Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task SaveRepository_ManagesSlots()
|
||||||
|
{
|
||||||
|
var root = CreateTempRoot();
|
||||||
|
using var storage = new FileStorage(root, new JsonSerializer());
|
||||||
|
var config = new SaveConfiguration
|
||||||
|
{
|
||||||
|
SaveRoot = "saves",
|
||||||
|
SaveSlotPrefix = "slot_",
|
||||||
|
SaveFileName = "save.json"
|
||||||
|
};
|
||||||
|
|
||||||
|
var repository = new SaveRepository<TestSaveData>(storage, config);
|
||||||
|
var data = new TestSaveData { Name = "hero" };
|
||||||
|
|
||||||
|
await repository.SaveAsync(1, data);
|
||||||
|
Assert.That(await repository.ExistsAsync(1));
|
||||||
|
|
||||||
|
var loaded = await repository.LoadAsync(1);
|
||||||
|
Assert.That(loaded.Name, Is.EqualTo(data.Name));
|
||||||
|
|
||||||
|
var slots = await repository.ListSlotsAsync();
|
||||||
|
Assert.That(slots, Is.EqualTo(new[] { 1 }));
|
||||||
|
|
||||||
|
await repository.DeleteAsync(1);
|
||||||
|
Assert.That(await repository.ExistsAsync(1), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task UnifiedSettingsDataRepository_RoundTripsDataAndLoadAll()
|
||||||
|
{
|
||||||
|
var root = CreateTempRoot();
|
||||||
|
using var storage = new FileStorage(root, new JsonSerializer());
|
||||||
|
var serializer = new JsonSerializer();
|
||||||
|
var repo = new UnifiedSettingsDataRepository(
|
||||||
|
storage,
|
||||||
|
serializer,
|
||||||
|
new DataRepositoryOptions { EnableEvents = false },
|
||||||
|
"settings.json");
|
||||||
|
|
||||||
|
var location = new TestDataLocation("settings/choice");
|
||||||
|
repo.RegisterDataType(location, typeof(TestSimpleData));
|
||||||
|
|
||||||
|
var data = new TestSimpleData { Value = 42 };
|
||||||
|
await repo.SaveAsync(location, data);
|
||||||
|
|
||||||
|
using var storage2 = new FileStorage(root, new JsonSerializer());
|
||||||
|
var repo2 = new UnifiedSettingsDataRepository(
|
||||||
|
storage2,
|
||||||
|
serializer,
|
||||||
|
new DataRepositoryOptions { EnableEvents = false },
|
||||||
|
"settings.json");
|
||||||
|
repo2.RegisterDataType(location, typeof(TestSimpleData));
|
||||||
|
|
||||||
|
var loaded = await repo2.LoadAsync<TestSimpleData>(location);
|
||||||
|
Assert.That(loaded.Value, Is.EqualTo(data.Value));
|
||||||
|
|
||||||
|
var all = await repo2.LoadAllAsync();
|
||||||
|
Assert.That(all.Keys, Contains.Item(location.Key));
|
||||||
|
Assert.That(all[location.Key], Is.TypeOf<TestSimpleData>());
|
||||||
|
}
|
||||||
|
}
|
||||||
177
GFramework.Game.Tests/Serializer/JsonSerializerTests.cs
Normal file
177
GFramework.Game.Tests/Serializer/JsonSerializerTests.cs
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using GameJsonSerializer = GFramework.Game.Serializer.JsonSerializer;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Serializer;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class JsonSerializerTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void Serialize_And_Deserialize_Should_RoundTrip_Object()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
var original = new PlayerStateStub
|
||||||
|
{
|
||||||
|
Name = "Player1",
|
||||||
|
Level = 7
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = serializer.Serialize(original);
|
||||||
|
var restored = serializer.Deserialize<PlayerStateStub>(json);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(restored.Name, Is.EqualTo("Player1"));
|
||||||
|
Assert.That(restored.Level, Is.EqualTo(7));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Serialize_Should_Honor_Injected_Settings()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer(new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var json = serializer.Serialize(new OptionalStateStub
|
||||||
|
{
|
||||||
|
Name = "Configured",
|
||||||
|
Description = null
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(json, Does.Contain(Environment.NewLine));
|
||||||
|
Assert.That(json, Does.Contain("\"Name\": \"Configured\""));
|
||||||
|
Assert.That(json, Does.Not.Contain("Description"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Converters_Should_Be_Used_For_Serialization_And_Deserialization()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
serializer.Converters.Add(new CoordinateStubConverter());
|
||||||
|
|
||||||
|
var json = serializer.Serialize(new CoordinateStub { X = 3, Y = 9 });
|
||||||
|
var restored = serializer.Deserialize<CoordinateStub>(json);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(json, Is.EqualTo("\"3:9\""));
|
||||||
|
Assert.That(restored.X, Is.EqualTo(3));
|
||||||
|
Assert.That(restored.Y, Is.EqualTo(9));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deserialize_Should_Throw_With_Target_Type_Context_When_Json_Is_Invalid()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
|
||||||
|
var exception =
|
||||||
|
Assert.Throws<InvalidOperationException>(() => serializer.Deserialize<PlayerStateStub>("{invalid json}"));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Message, Does.Contain(typeof(PlayerStateStub).FullName));
|
||||||
|
Assert.That(exception.InnerException, Is.Not.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deserialize_With_Runtime_Type_Should_Throw_With_Target_Type_Context_When_Json_Is_Invalid()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
|
||||||
|
var exception =
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
serializer.Deserialize("{invalid json}", typeof(PlayerStateStub)));
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
Assert.That(exception!.Message, Does.Contain(typeof(PlayerStateStub).FullName));
|
||||||
|
Assert.That(exception.InnerException, Is.Not.Null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deserialize_With_Runtime_Type_Should_Return_Target_Runtime_Type()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
|
||||||
|
var restored = serializer.Deserialize("{\"Name\":\"Runtime\",\"Level\":11}", typeof(PlayerStateStub));
|
||||||
|
|
||||||
|
Assert.That(restored, Is.TypeOf<PlayerStateStub>());
|
||||||
|
Assert.That(((PlayerStateStub)restored).Level, Is.EqualTo(11));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Serialize_With_Runtime_Type_Should_Allow_Null_Object()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
|
||||||
|
var json = serializer.Serialize(null!, typeof(PlayerStateStub));
|
||||||
|
|
||||||
|
Assert.That(json, Is.EqualTo("null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Deserialize_Should_Preserve_ArgumentException_For_Invalid_Input_Arguments()
|
||||||
|
{
|
||||||
|
var serializer = new GameJsonSerializer();
|
||||||
|
|
||||||
|
var exception = Assert.Throws<ArgumentException>(() => serializer.Deserialize<PlayerStateStub>(string.Empty));
|
||||||
|
|
||||||
|
Assert.That(exception, Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PlayerStateStub
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int Level { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class OptionalStateStub
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CoordinateStub
|
||||||
|
{
|
||||||
|
public int X { get; set; }
|
||||||
|
|
||||||
|
public int Y { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CoordinateStubConverter : JsonConverter<CoordinateStub>
|
||||||
|
{
|
||||||
|
public override void WriteJson(JsonWriter writer, CoordinateStub? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
writer.WriteValue($"{value?.X}:{value?.Y}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override CoordinateStub ReadJson(
|
||||||
|
JsonReader reader,
|
||||||
|
Type objectType,
|
||||||
|
CoordinateStub? existingValue,
|
||||||
|
bool hasExistingValue,
|
||||||
|
JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var raw = (string?)reader.Value ?? throw new JsonSerializationException("Coordinate value cannot be null.");
|
||||||
|
var parts = raw.Split(':');
|
||||||
|
return new CoordinateStub
|
||||||
|
{
|
||||||
|
X = int.Parse(parts[0]),
|
||||||
|
Y = int.Parse(parts[1])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
181
GFramework.Game.Tests/Setting/SettingsModelTests.cs
Normal file
181
GFramework.Game.Tests/Setting/SettingsModelTests.cs
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Core.Abstractions.Lifecycle;
|
||||||
|
using GFramework.Core.Abstractions.Rule;
|
||||||
|
using GFramework.Game.Abstractions.Data;
|
||||||
|
using GFramework.Game.Abstractions.Enums;
|
||||||
|
using GFramework.Game.Abstractions.Setting;
|
||||||
|
using GFramework.Game.Setting;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Setting;
|
||||||
|
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class SettingsModelTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void GetData_After_Initialize_Should_Register_New_Type_In_Repository()
|
||||||
|
{
|
||||||
|
var locationProvider = new TestDataLocationProvider();
|
||||||
|
var repository = new FakeSettingsDataRepository();
|
||||||
|
var model = new SettingsModel<FakeSettingsDataRepository>(locationProvider, repository);
|
||||||
|
((IContextAware)model).SetContext(new Mock<IArchitectureContext>(MockBehavior.Loose).Object);
|
||||||
|
|
||||||
|
((IInitializable)model).Initialize();
|
||||||
|
|
||||||
|
_ = model.GetData<TestSettingsData>();
|
||||||
|
|
||||||
|
Assert.That(repository.RegisteredTypes, Contains.Key("TestSettingsData"));
|
||||||
|
Assert.That(repository.RegisteredTypes["TestSettingsData"], Is.EqualTo(typeof(TestSettingsData)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task RegisterMigration_After_Cache_Warmup_Should_Invalidate_Type_Cache()
|
||||||
|
{
|
||||||
|
var locationProvider = new TestDataLocationProvider();
|
||||||
|
var repository = new FakeSettingsDataRepository();
|
||||||
|
var model = new SettingsModel<FakeSettingsDataRepository>(locationProvider, repository);
|
||||||
|
((IContextAware)model).SetContext(new Mock<IArchitectureContext>(MockBehavior.Loose).Object);
|
||||||
|
|
||||||
|
_ = model.GetData<TestSettingsData>();
|
||||||
|
((IInitializable)model).Initialize();
|
||||||
|
|
||||||
|
repository.Stored["TestSettingsData"] = new TestSettingsData
|
||||||
|
{
|
||||||
|
Version = 1,
|
||||||
|
Value = "legacy"
|
||||||
|
};
|
||||||
|
|
||||||
|
await model.InitializeAsync();
|
||||||
|
Assert.That(model.GetData<TestSettingsData>().Version, Is.EqualTo(1));
|
||||||
|
|
||||||
|
model.RegisterMigration(new TestSettingsMigration());
|
||||||
|
|
||||||
|
repository.Stored["TestSettingsData"] = new TestSettingsData
|
||||||
|
{
|
||||||
|
Version = 1,
|
||||||
|
Value = "legacy"
|
||||||
|
};
|
||||||
|
|
||||||
|
await model.InitializeAsync();
|
||||||
|
|
||||||
|
var current = model.GetData<TestSettingsData>();
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(current.Version, Is.EqualTo(2));
|
||||||
|
Assert.That(current.Value, Is.EqualTo("legacy-migrated"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestSettingsData : ISettingsData
|
||||||
|
{
|
||||||
|
public string Value { get; set; } = "default";
|
||||||
|
|
||||||
|
public int Version { get; set; } = 1;
|
||||||
|
|
||||||
|
public DateTime LastModified { get; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Value = "default";
|
||||||
|
Version = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadFrom(ISettingsData source)
|
||||||
|
{
|
||||||
|
if (source is not TestSettingsData data)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value = data.Value;
|
||||||
|
Version = data.Version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestSettingsMigration : ISettingsMigration
|
||||||
|
{
|
||||||
|
public Type SettingsType => typeof(TestSettingsData);
|
||||||
|
|
||||||
|
public int FromVersion => 1;
|
||||||
|
|
||||||
|
public int ToVersion => 2;
|
||||||
|
|
||||||
|
public ISettingsSection Migrate(ISettingsSection oldData)
|
||||||
|
{
|
||||||
|
var data = (TestSettingsData)oldData;
|
||||||
|
return new TestSettingsData
|
||||||
|
{
|
||||||
|
Version = 2,
|
||||||
|
Value = $"{data.Value}-migrated"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FakeSettingsDataRepository : ISettingsDataRepository
|
||||||
|
{
|
||||||
|
public Dictionary<string, Type> RegisteredTypes { get; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public Dictionary<string, IData> Stored { get; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
public Task<T> LoadAsync<T>(IDataLocation location) where T : class, IData, new()
|
||||||
|
{
|
||||||
|
return Task.FromResult(Stored.TryGetValue(location.Key, out var data) ? (T)data : new T());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveAsync<T>(IDataLocation location, T data) where T : class, IData
|
||||||
|
{
|
||||||
|
Stored[location.Key] = data;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> ExistsAsync(IDataLocation location)
|
||||||
|
{
|
||||||
|
return Task.FromResult(Stored.ContainsKey(location.Key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DeleteAsync(IDataLocation location)
|
||||||
|
{
|
||||||
|
Stored.Remove(location.Key);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveAllAsync(IEnumerable<(IDataLocation location, IData data)> dataList)
|
||||||
|
{
|
||||||
|
foreach (var (location, data) in dataList)
|
||||||
|
{
|
||||||
|
Stored[location.Key] = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IDictionary<string, IData>> LoadAllAsync()
|
||||||
|
{
|
||||||
|
IDictionary<string, IData> snapshot = new Dictionary<string, IData>(Stored, StringComparer.Ordinal);
|
||||||
|
return Task.FromResult(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterDataType(IDataLocation location, Type type)
|
||||||
|
{
|
||||||
|
RegisteredTypes[location.Key] = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDataLocationProvider : IDataLocationProvider
|
||||||
|
{
|
||||||
|
public IDataLocation GetLocation(Type type)
|
||||||
|
{
|
||||||
|
return new TestDataLocation(type.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDataLocation(string key) : IDataLocation
|
||||||
|
{
|
||||||
|
public string Key { get; } = key;
|
||||||
|
|
||||||
|
public StorageKinds Kinds => StorageKinds.Memory;
|
||||||
|
|
||||||
|
public string? Namespace => "tests";
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string>? Metadata => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -80,11 +80,10 @@ public class UnifiedSettingsDataRepository(
|
|||||||
public async Task SaveAsync<T>(IDataLocation location, T data)
|
public async Task SaveAsync<T>(IDataLocation location, T data)
|
||||||
where T : class, IData
|
where T : class, IData
|
||||||
{
|
{
|
||||||
|
await EnsureLoadedAsync();
|
||||||
await _lock.WaitAsync();
|
await _lock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await EnsureLoadedAsync();
|
|
||||||
|
|
||||||
var key = location.Key;
|
var key = location.Key;
|
||||||
var serialized = Serializer.Serialize(data);
|
var serialized = Serializer.Serialize(data);
|
||||||
|
|
||||||
@ -211,7 +210,9 @@ public class UnifiedSettingsDataRepository(
|
|||||||
|
|
||||||
var key = UnifiedKey;
|
var key = UnifiedKey;
|
||||||
|
|
||||||
_file = await Storage.ExistsAsync(key) ? await Storage.ReadAsync<UnifiedSettingsFile>(key) : new UnifiedSettingsFile { Version = 1 };
|
_file = await Storage.ExistsAsync(key)
|
||||||
|
? await Storage.ReadAsync<UnifiedSettingsFile>(key)
|
||||||
|
: new UnifiedSettingsFile { Version = 1 };
|
||||||
|
|
||||||
_loaded = true;
|
_loaded = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,27 @@ namespace GFramework.Game.Serializer;
|
|||||||
public sealed class JsonSerializer
|
public sealed class JsonSerializer
|
||||||
: IRuntimeTypeSerializer
|
: IRuntimeTypeSerializer
|
||||||
{
|
{
|
||||||
|
private readonly JsonSerializerSettings _settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化 JSON 序列化器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="settings">可选的 Newtonsoft.Json 配置;不提供时使用默认配置。</param>
|
||||||
|
public JsonSerializer(JsonSerializerSettings? settings = null)
|
||||||
|
{
|
||||||
|
_settings = settings ?? new JsonSerializerSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前序列化器使用的 Newtonsoft.Json 配置实例。
|
||||||
|
/// </summary>
|
||||||
|
public JsonSerializerSettings Settings => _settings;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前序列化器使用的自定义转换器集合。
|
||||||
|
/// </summary>
|
||||||
|
public IList<JsonConverter> Converters => _settings.Converters;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将指定类型的对象序列化为JSON字符串
|
/// 将指定类型的对象序列化为JSON字符串
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -17,21 +38,9 @@ public sealed class JsonSerializer
|
|||||||
/// <returns>序列化后的JSON字符串</returns>
|
/// <returns>序列化后的JSON字符串</returns>
|
||||||
public string Serialize<T>(T value)
|
public string Serialize<T>(T value)
|
||||||
{
|
{
|
||||||
return JsonConvert.SerializeObject(value);
|
return JsonConvert.SerializeObject(value, _settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将JSON字符串反序列化为指定类型的对象
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">要反序列化的目标类型</typeparam>
|
|
||||||
/// <param name="data">要反序列化的JSON字符串数据</param>
|
|
||||||
/// <returns>反序列化后的对象实例</returns>
|
|
||||||
/// <exception cref="ArgumentException">当无法反序列化数据时抛出</exception>
|
|
||||||
public T Deserialize<T>(string data)
|
|
||||||
{
|
|
||||||
return JsonConvert.DeserializeObject<T>(data)
|
|
||||||
?? throw new ArgumentException("Cannot deserialize data");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将对象序列化为JSON字符串(使用运行时类型)
|
/// 将对象序列化为JSON字符串(使用运行时类型)
|
||||||
@ -41,7 +50,24 @@ public sealed class JsonSerializer
|
|||||||
/// <returns>序列化后的JSON字符串</returns>
|
/// <returns>序列化后的JSON字符串</returns>
|
||||||
public string Serialize(object obj, Type type)
|
public string Serialize(object obj, Type type)
|
||||||
{
|
{
|
||||||
return JsonConvert.SerializeObject(obj, type, null);
|
ArgumentNullException.ThrowIfNull(type);
|
||||||
|
|
||||||
|
return JsonConvert.SerializeObject(obj, type, _settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将JSON字符串反序列化为指定类型的对象
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">要反序列化的目标类型</typeparam>
|
||||||
|
/// <param name="data">要反序列化的JSON字符串数据</param>
|
||||||
|
/// <returns>反序列化后的对象实例</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">当无法反序列化数据时抛出</exception>
|
||||||
|
public T Deserialize<T>(string data)
|
||||||
|
{
|
||||||
|
return (T)DeserializeCore(
|
||||||
|
data,
|
||||||
|
typeof(T),
|
||||||
|
static (json, _, settings) => JsonConvert.DeserializeObject<T>(json, settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -50,10 +76,43 @@ public sealed class JsonSerializer
|
|||||||
/// <param name="data">要反序列化的JSON字符串数据</param>
|
/// <param name="data">要反序列化的JSON字符串数据</param>
|
||||||
/// <param name="type">反序列化目标类型</param>
|
/// <param name="type">反序列化目标类型</param>
|
||||||
/// <returns>反序列化后的对象实例</returns>
|
/// <returns>反序列化后的对象实例</returns>
|
||||||
/// <exception cref="ArgumentException">当无法反序列化到指定类型时抛出</exception>
|
/// <exception cref="InvalidOperationException">当无法反序列化到指定类型时抛出</exception>
|
||||||
public object Deserialize(string data, Type type)
|
public object Deserialize(string data, Type type)
|
||||||
{
|
{
|
||||||
return JsonConvert.DeserializeObject(data, type)
|
return DeserializeCore(
|
||||||
?? throw new ArgumentException($"Cannot deserialize to {type.Name}");
|
data,
|
||||||
|
type,
|
||||||
|
static (json, targetType, settings) => JsonConvert.DeserializeObject(json, targetType, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
private object DeserializeCore(
|
||||||
|
string data,
|
||||||
|
Type targetType,
|
||||||
|
Func<string, Type, JsonSerializerSettings, object?> deserialize)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrWhiteSpace(data);
|
||||||
|
ArgumentNullException.ThrowIfNull(targetType);
|
||||||
|
ArgumentNullException.ThrowIfNull(deserialize);
|
||||||
|
|
||||||
|
object? result;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = deserialize(data, targetType, _settings);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not ArgumentException)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Failed to deserialize JSON to target type '{targetType.FullName}'.",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Deserialization returned null for target type '{targetType.FullName}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -59,7 +59,9 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
public T GetData<T>() where T : class, ISettingsData, new()
|
public T GetData<T>() where T : class, ISettingsData, new()
|
||||||
{
|
{
|
||||||
// 使用_data字典获取或添加指定类型的实例,确保唯一性
|
// 使用_data字典获取或添加指定类型的实例,确保唯一性
|
||||||
return (T)_data.GetOrAdd(typeof(T), _ => new T());
|
var data = (T)_data.GetOrAdd(typeof(T), _ => new T());
|
||||||
|
TryRegisterDataType(typeof(T));
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -84,6 +86,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
{
|
{
|
||||||
_applicators[typeof(T)] = applicator;
|
_applicators[typeof(T)] = applicator;
|
||||||
_data[applicator.DataType] = applicator.Data;
|
_data[applicator.DataType] = applicator.Data;
|
||||||
|
TryRegisterDataType(applicator.DataType);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +117,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
public ISettingsModel RegisterMigration(ISettingsMigration migration)
|
public ISettingsModel RegisterMigration(ISettingsMigration migration)
|
||||||
{
|
{
|
||||||
_migrations[(migration.SettingsType, migration.FromVersion)] = migration;
|
_migrations[(migration.SettingsType, migration.FromVersion)] = migration;
|
||||||
|
_migrationCache.TryRemove(migration.SettingsType, out _);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,11 +262,21 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
// 遍历所有已知的数据类型,为其分配位置并注册到数据仓库中
|
// 遍历所有已知的数据类型,为其分配位置并注册到数据仓库中
|
||||||
foreach (var type in _data.Keys)
|
foreach (var type in _data.Keys)
|
||||||
{
|
{
|
||||||
var location = _locationProvider.GetLocation(type);
|
TryRegisterDataType(type);
|
||||||
DataRepository.RegisterDataType(location, type);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void TryRegisterDataType(Type type)
|
||||||
|
{
|
||||||
|
if (_repository == null || _locationProvider == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = _locationProvider.GetLocation(type);
|
||||||
|
_repository.RegisterDataType(location, type);
|
||||||
|
}
|
||||||
|
|
||||||
private ISettingsData MigrateIfNeeded(ISettingsData data)
|
private ISettingsData MigrateIfNeeded(ISettingsData data)
|
||||||
{
|
{
|
||||||
if (data is not IVersionedData versioned)
|
if (data is not IVersionedData versioned)
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
---
|
---
|
||||||
title: 数据与存档系统
|
title: 数据与存档系统
|
||||||
description: 数据与存档系统提供了完整的数据持久化解决方案,支持多槽位存档、版本管理和数据迁移。
|
description: 数据与存档系统提供统一的数据持久化基础能力,支持多槽位存档、版本化数据和仓库抽象。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 数据与存档系统
|
# 数据与存档系统
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、数据版本控制和自动迁移,让你可以轻松实现游戏存档、设置保存等功能。
|
数据与存档系统是 GFramework.Game 中用于管理游戏数据持久化的核心组件。它提供了统一的数据加载和保存接口,支持多槽位存档管理、版本化数据模式,以及与存储系统、序列化系统的组合使用。
|
||||||
|
|
||||||
通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在数据结构变化时自动进行版本迁移。
|
通过数据系统,你可以将游戏数据保存到本地存储,支持多个存档槽位,并在需要时于应用层实现版本迁移。
|
||||||
|
|
||||||
**主要特性**:
|
**主要特性**:
|
||||||
|
|
||||||
- 统一的数据持久化接口
|
- 统一的数据持久化接口
|
||||||
- 多槽位存档管理
|
- 多槽位存档管理
|
||||||
- 数据版本控制和迁移
|
- 数据版本控制模式
|
||||||
- 异步加载和保存
|
- 异步加载和保存
|
||||||
- 批量数据操作
|
- 批量数据操作
|
||||||
- 与存储系统集成
|
- 与存储系统集成
|
||||||
@ -69,10 +69,11 @@ public interface ISaveRepository<TSaveData> : IUtility
|
|||||||
`IVersionedData` 支持数据版本管理:
|
`IVersionedData` 支持数据版本管理:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface IVersionedData : IData
|
public interface IVersionedData : IData
|
||||||
{
|
{
|
||||||
int Version { get; set; }
|
int Version { get; }
|
||||||
}
|
DateTime LastModified { get; }
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 基本用法
|
## 基本用法
|
||||||
@ -262,9 +263,13 @@ public partial class AutoSaveController : IController
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 数据版本迁移
|
### 数据版本迁移
|
||||||
|
|
||||||
```csharp
|
`SaveRepository<TSaveData>` 当前负责槽位存档的读取、写入、删除和列举,并没有内建“注册迁移器后自动升级存档”的统一迁移管线。
|
||||||
|
|
||||||
|
下面示例展示的是应用层迁移策略:加载后检查版本,调用你自己的迁移逻辑,再决定是否回写新版本数据。
|
||||||
|
|
||||||
|
```csharp
|
||||||
// 版本 1 的数据
|
// 版本 1 的数据
|
||||||
public class SaveDataV1 : IVersionedData
|
public class SaveDataV1 : IVersionedData
|
||||||
{
|
{
|
||||||
@ -299,18 +304,18 @@ public class SaveDataMigrator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载时自动迁移
|
// 加载后由应用层决定是否迁移
|
||||||
public async Task<SaveDataV2> LoadWithMigration(int slot)
|
public async Task<SaveDataV2> LoadWithMigration(int slot)
|
||||||
{
|
{
|
||||||
var saveRepo = this.GetUtility<ISaveRepository<SaveDataV2>>();
|
var saveRepo = this.GetUtility<ISaveRepository<SaveDataV2>>();
|
||||||
var data = await saveRepo.LoadAsync(slot);
|
var data = await saveRepo.LoadAsync(slot);
|
||||||
|
|
||||||
if (data.Version < 2)
|
if (data.Version < 2)
|
||||||
{
|
{
|
||||||
// 需要迁移
|
// 需要迁移:此处调用应用层迁移器
|
||||||
var oldData = data as SaveDataV1;
|
var oldData = data as SaveDataV1;
|
||||||
var migrator = new SaveDataMigrator();
|
var migrator = new SaveDataMigrator();
|
||||||
var newData = migrator.Migrate(oldData);
|
var newData = migrator.Migrate(oldData);
|
||||||
|
|
||||||
// 保存迁移后的数据
|
// 保存迁移后的数据
|
||||||
await saveRepo.SaveAsync(slot, newData);
|
await saveRepo.SaveAsync(slot, newData);
|
||||||
@ -506,10 +511,10 @@ await saveRepo.SaveAsync(2, saveData); // 槽位 2
|
|||||||
await saveRepo.SaveAsync(3, saveData); // 槽位 3
|
await saveRepo.SaveAsync(3, saveData); // 槽位 3
|
||||||
```
|
```
|
||||||
|
|
||||||
### 问题:如何处理数据版本升级?
|
### 问题:如何处理数据版本升级?
|
||||||
|
|
||||||
**解答**:
|
**解答**:
|
||||||
实现 `IVersionedData` 并在加载时检查版本:
|
实现 `IVersionedData` 并在加载后检查版本。当前框架不会自动为 `ISaveRepository<T>` 执行迁移,需要由业务层决定迁移规则与回写时机:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
var data = await saveRepo.LoadAsync(slot);
|
var data = await saveRepo.LoadAsync(slot);
|
||||||
|
|||||||
@ -675,6 +675,7 @@ public class CachedStorage : IStorage
|
|||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
using GFramework.Game.Serializer;
|
using GFramework.Game.Serializer;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
public class GameDataSerializer
|
public class GameDataSerializer
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,192 +1,199 @@
|
|||||||
# 设置系统 (Settings System)
|
# 设置系统
|
||||||
|
|
||||||
## 概述
|
设置系统负责管理 `ISettingsData`、持久化加载/保存,以及把设置真正应用到运行时环境。
|
||||||
|
|
||||||
设置系统是 GFramework.Game 的核心组件之一,负责管理游戏中各种设置配置。该系统采用了模型-系统分离的设计模式,支持设置部分(Section)的管理和设置应用器模式。
|
当前实现以 `SettingsModel<TRepository>` 和 `SettingsSystem` 为核心,已经不是旧文档中的
|
||||||
|
`Get<T>() / Register(IApplyAbleSettings)` 接口模型。
|
||||||
|
|
||||||
## 核心类
|
## 核心概念
|
||||||
|
|
||||||
### SettingsModel
|
### ISettingsData
|
||||||
|
|
||||||
设置模型类,继承自 `AbstractModel` 并实现 `ISettingsModel` 接口。
|
设置数据对象负责保存设置值、提供默认值,并在加载后把外部数据回填到当前实例。
|
||||||
|
|
||||||
**主要功能:**
|
|
||||||
|
|
||||||
- 管理不同类型的设置部分(Settings Section)
|
|
||||||
- 提供类型安全的设置访问
|
|
||||||
- 支持可应用设置对象的注册
|
|
||||||
|
|
||||||
**关键方法:**
|
|
||||||
|
|
||||||
- `Get<T>()` - 获取或创建指定类型的设置部分
|
|
||||||
- `TryGet(Type, out ISettingsSection)` - 尝试获取设置部分
|
|
||||||
- `Register(IApplyAbleSettings)` - 注册可应用的设置对象
|
|
||||||
- `All()` - 获取所有设置部分
|
|
||||||
|
|
||||||
### SettingsSystem
|
|
||||||
|
|
||||||
设置系统类,继承自 `AbstractSystem` 并实现 `ISettingsSystem` 接口。
|
|
||||||
|
|
||||||
**主要功能:**
|
|
||||||
|
|
||||||
- 应用设置配置到相应系统
|
|
||||||
- 支持单个或批量设置应用
|
|
||||||
- 自动识别可应用设置类型
|
|
||||||
|
|
||||||
**关键方法:**
|
|
||||||
|
|
||||||
- `ApplyAll()` - 应用所有设置配置
|
|
||||||
- `Apply<T>()` - 应用指定类型的设置
|
|
||||||
- `Apply(IEnumerable<Type>)` - 应用指定类型集合的设置
|
|
||||||
|
|
||||||
## 架构设计
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD
|
|
||||||
A[ISettingsModel] --> B[SettingsModel]
|
|
||||||
C[ISettingsSystem] --> D[SettingsSystem]
|
|
||||||
|
|
||||||
B --> E[Dictionary<Type, ISettingsSection>]
|
|
||||||
D --> B
|
|
||||||
|
|
||||||
F[ISettingsSection] --> G[IApplyAbleSettings]
|
|
||||||
H[AudioSettings] --> G
|
|
||||||
I[GraphicsSettings] --> G
|
|
||||||
|
|
||||||
E --> H
|
|
||||||
E --> I
|
|
||||||
|
|
||||||
J[Application] --> D
|
|
||||||
D --> G
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用示例
|
|
||||||
|
|
||||||
### 基本使用
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 获取设置模型
|
public interface ISettingsData : IResettable, IVersionedData, ILoadableFrom<ISettingsData>;
|
||||||
var settingsModel = this.GetModel<ISettingsModel>();
|
|
||||||
|
|
||||||
// 获取或创建音频设置
|
|
||||||
var audioSettings = settingsModel.Get<GodotAudioSettings>();
|
|
||||||
audioSettings.MasterVolume = 0.8f;
|
|
||||||
audioSettings.BgmVolume = 0.6f;
|
|
||||||
audioSettings.SfxVolume = 0.9f;
|
|
||||||
|
|
||||||
// 注册设置到模型
|
|
||||||
settingsModel.Register(audioSettings);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 应用设置
|
这意味着一个设置数据类型通常需要实现:
|
||||||
|
|
||||||
|
- `Reset()`:恢复默认值
|
||||||
|
- `Version` / `LastModified`:暴露版本化信息
|
||||||
|
- `LoadFrom(ISettingsData)`:把已加载或迁移后的数据复制到当前实例
|
||||||
|
|
||||||
|
### IResetApplyAbleSettings
|
||||||
|
|
||||||
|
应用器负责把设置数据作用到引擎或运行时环境:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
// 获取设置系统
|
public interface IResetApplyAbleSettings : IResettable, IApplyAbleSettings
|
||||||
var settingsSystem = this.GetSystem<ISettingsSystem>();
|
|
||||||
|
|
||||||
// 应用所有设置
|
|
||||||
await settingsSystem.ApplyAll();
|
|
||||||
|
|
||||||
// 应用特定类型设置
|
|
||||||
await settingsSystem.Apply<GodotAudioSettings>();
|
|
||||||
|
|
||||||
// 应用多个类型设置
|
|
||||||
var types = new[] { typeof(GodotAudioSettings), typeof(GodotGraphicsSettings) };
|
|
||||||
await settingsSystem.Apply(types);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 创建自定义设置
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class GameSettings : ISettingsSection
|
|
||||||
{
|
{
|
||||||
public float GameSpeed { get; set; } = 1.0f;
|
ISettingsData Data { get; }
|
||||||
public int Difficulty { get; set; } = 1;
|
Type DataType { get; }
|
||||||
public bool AutoSave { get; set; } = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用自定义设置
|
|
||||||
var gameSettings = settingsModel.Get<GameSettings>();
|
|
||||||
gameSettings.GameSpeed = 1.5f;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 创建可应用设置
|
常见用途包括:
|
||||||
|
|
||||||
|
- 把音量设置同步到音频总线
|
||||||
|
- 把图形设置同步到窗口系统
|
||||||
|
- 把语言设置同步到本地化管理器
|
||||||
|
|
||||||
|
## ISettingsModel
|
||||||
|
|
||||||
|
当前 `ISettingsModel` 的主要 API 如下:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public class GameSettings : ISettingsSection, IApplyAbleSettings
|
public interface ISettingsModel : IModel
|
||||||
|
{
|
||||||
|
bool IsInitialized { get; }
|
||||||
|
|
||||||
|
T GetData<T>() where T : class, ISettingsData, new();
|
||||||
|
IEnumerable<ISettingsData> AllData();
|
||||||
|
|
||||||
|
ISettingsModel RegisterApplicator<T>(T applicator)
|
||||||
|
where T : class, IResetApplyAbleSettings;
|
||||||
|
T? GetApplicator<T>() where T : class, IResetApplyAbleSettings;
|
||||||
|
IEnumerable<IResetApplyAbleSettings> AllApplicators();
|
||||||
|
|
||||||
|
ISettingsModel RegisterMigration(ISettingsMigration migration);
|
||||||
|
|
||||||
|
Task InitializeAsync();
|
||||||
|
Task SaveAllAsync();
|
||||||
|
Task ApplyAllAsync();
|
||||||
|
void Reset<T>() where T : class, ISettingsData, new();
|
||||||
|
void ResetAll();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
行为说明:
|
||||||
|
|
||||||
|
- `GetData<T>()` 返回某个设置数据的唯一实例
|
||||||
|
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
|
||||||
|
- `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
|
||||||
|
- `SaveAllAsync()` 持久化当前所有设置数据
|
||||||
|
- `ApplyAllAsync()` 依次调用所有 applicator 的 `Apply()`
|
||||||
|
|
||||||
|
## SettingsSystem
|
||||||
|
|
||||||
|
`SettingsSystem` 是对模型的系统级封装,面向业务代码提供更直接的入口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ISettingsSystem : ISystem
|
||||||
|
{
|
||||||
|
Task ApplyAll();
|
||||||
|
Task Apply<T>() where T : class, IResetApplyAbleSettings;
|
||||||
|
Task SaveAll();
|
||||||
|
Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new();
|
||||||
|
Task ResetAll();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
它不会自己保存数据,而是把保存、重置和应用逻辑委托给 `ISettingsModel`。
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
### 定义设置数据
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class GameplaySettings : ISettingsData
|
||||||
{
|
{
|
||||||
public float GameSpeed { get; set; } = 1.0f;
|
public float GameSpeed { get; set; } = 1.0f;
|
||||||
public int Difficulty { get; set; } = 1;
|
|
||||||
|
public int Version { get; private set; } = 1;
|
||||||
|
public DateTime LastModified { get; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
GameSpeed = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LoadFrom(ISettingsData source)
|
||||||
|
{
|
||||||
|
if (source is not GameplaySettings settings)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GameSpeed = settings.GameSpeed;
|
||||||
|
Version = settings.Version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 定义 applicator
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public sealed class GameplaySettingsApplicator : IResetApplyAbleSettings
|
||||||
|
{
|
||||||
|
public GameplaySettingsApplicator(GameplaySettings data)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ISettingsData Data { get; }
|
||||||
|
public Type DataType => typeof(GameplaySettings);
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Data.Reset();
|
||||||
|
}
|
||||||
|
|
||||||
public Task Apply()
|
public Task Apply()
|
||||||
{
|
{
|
||||||
// 应用游戏速度
|
var settings = (GameplaySettings)Data;
|
||||||
Time.timeScale = GameSpeed;
|
TimeScale.Current = settings.GameSpeed;
|
||||||
|
|
||||||
// 应用难度设置
|
|
||||||
GameDifficulty.Current = Difficulty;
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 接口定义
|
### 使用模型和系统
|
||||||
|
|
||||||
### ISettingsSection
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface ISettingsSection
|
var settingsModel = this.GetModel<ISettingsModel>();
|
||||||
|
|
||||||
|
var gameplayData = settingsModel.GetData<GameplaySettings>();
|
||||||
|
gameplayData.GameSpeed = 1.25f;
|
||||||
|
|
||||||
|
settingsModel.RegisterApplicator(new GameplaySettingsApplicator(gameplayData));
|
||||||
|
|
||||||
|
await settingsModel.InitializeAsync();
|
||||||
|
await settingsModel.SaveAllAsync();
|
||||||
|
|
||||||
|
var settingsSystem = this.GetSystem<ISettingsSystem>();
|
||||||
|
await settingsSystem.ApplyAll();
|
||||||
|
```
|
||||||
|
|
||||||
|
## 迁移
|
||||||
|
|
||||||
|
设置系统内建了迁移注册入口:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface ISettingsMigration
|
||||||
{
|
{
|
||||||
// 设置部分的标识接口
|
Type SettingsType { get; }
|
||||||
|
int FromVersion { get; }
|
||||||
|
int ToVersion { get; }
|
||||||
|
ISettingsData Migrate(ISettingsData oldData);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### IApplyAbleSettings
|
当 `InitializeAsync()` 读取到旧版本设置时,会按已注册迁移链逐步升级,再通过 `LoadFrom` 回填到当前实例。
|
||||||
|
|
||||||
```csharp
|
## 依赖项
|
||||||
public interface IApplyAbleSettings : ISettingsSection
|
|
||||||
{
|
|
||||||
Task Apply();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ISettingsModel
|
要让设置系统完整工作,通常需要准备:
|
||||||
|
|
||||||
```csharp
|
- `ISettingsDataRepository`
|
||||||
public interface ISettingsModel
|
- `IDataLocationProvider`
|
||||||
{
|
- 一个具体的存储实现和序列化器
|
||||||
T Get<T>() where T : class, ISettingsSection, new();
|
|
||||||
bool TryGet(Type type, out ISettingsSection section);
|
|
||||||
IEnumerable<ISettingsSection> All();
|
|
||||||
void Register(IApplyAbleSettings applyAble);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ISettingsSystem
|
如果使用 `UnifiedSettingsDataRepository`,多个设置节会被合并到单个设置文件中统一保存。
|
||||||
|
|
||||||
```csharp
|
## 当前边界
|
||||||
public interface ISettingsSystem
|
|
||||||
{
|
|
||||||
Task ApplyAll();
|
|
||||||
Task Apply<T>() where T : class, ISettingsSection;
|
|
||||||
Task Apply(Type settingsType);
|
|
||||||
Task Apply(IEnumerable<Type> settingsTypes);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 设计模式
|
- 设置迁移是内建能力
|
||||||
|
- 设置持久化是内建能力
|
||||||
该系统使用了以下设计模式:
|
- 设置如何应用到具体引擎由 applicator 决定
|
||||||
|
- 存档系统的迁移能力不等同于设置系统;`ISaveRepository<T>` 当前仍需要业务层自己实现迁移策略
|
||||||
1. **Repository Pattern** - SettingsModel 作为设置数据的仓库
|
|
||||||
2. **Command Pattern** - IApplyAbleSettings 的 Apply 方法作为命令
|
|
||||||
3. **Factory Pattern** - Get`<T>`() 方法创建设置实例
|
|
||||||
4. **Template Method** - AbstractSystem 提供初始化模板
|
|
||||||
|
|
||||||
## 最佳实践
|
|
||||||
|
|
||||||
1. **设置分类** - 将相关设置组织到同一个设置类中
|
|
||||||
2. **延迟应用** - 批量修改后再应用,而不是每次修改都应用
|
|
||||||
3. **类型安全** - 使用泛型方法确保类型安全
|
|
||||||
4. **可测试性** - 通过接口实现便于单元测试
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user