From c0aa8ba70e7cf121f28a0c9b2ba7fd7d2cec4938 Mon Sep 17 00:00:00 2001
From: GeWuYou <95328647+GeWuYou@users.noreply.github.com>
Date: Mon, 30 Mar 2026 13:18:54 +0800
Subject: [PATCH] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E6=A0=B8=E5=BF=83?=
=?UTF-8?q?=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 实现了 ConfigRegistry 配置注册表,支持按名称注册和类型安全查询
- 创建了 InMemoryConfigTable 内存配置表,提供基于字典的只读配置存储
- 定义了 IConfigLoader、IConfigRegistry 和 IConfigTable 接口契约
- 添加了完整的单元测试验证配置表的注册、查询和类型检查功能
- 在项目文件中添加了新的代码文件夹结构
- 实现了配置表的覆盖策略以支持开发期热重载需求
---
.../Config/IConfigLoader.cs | 19 ++
.../Config/IConfigRegistry.cs | 75 ++++++++
.../Config/IConfigTable.cs | 65 +++++++
.../Config/ConfigRegistryTests.cs | 130 ++++++++++++++
.../Config/InMemoryConfigTableTests.cs | 72 ++++++++
GFramework.Game/Config/ConfigRegistry.cs | 162 ++++++++++++++++++
GFramework.Game/Config/InMemoryConfigTable.cs | 118 +++++++++++++
GFramework.csproj | 1 +
8 files changed, 642 insertions(+)
create mode 100644 GFramework.Game.Abstractions/Config/IConfigLoader.cs
create mode 100644 GFramework.Game.Abstractions/Config/IConfigRegistry.cs
create mode 100644 GFramework.Game.Abstractions/Config/IConfigTable.cs
create mode 100644 GFramework.Game.Tests/Config/ConfigRegistryTests.cs
create mode 100644 GFramework.Game.Tests/Config/InMemoryConfigTableTests.cs
create mode 100644 GFramework.Game/Config/ConfigRegistry.cs
create mode 100644 GFramework.Game/Config/InMemoryConfigTable.cs
diff --git a/GFramework.Game.Abstractions/Config/IConfigLoader.cs b/GFramework.Game.Abstractions/Config/IConfigLoader.cs
new file mode 100644
index 0000000..50abd08
--- /dev/null
+++ b/GFramework.Game.Abstractions/Config/IConfigLoader.cs
@@ -0,0 +1,19 @@
+using GFramework.Core.Abstractions.Utility;
+
+namespace GFramework.Game.Abstractions.Config;
+
+///
+/// 定义配置加载器契约。
+/// 具体实现负责从文件系统、资源包或其他配置源加载文本配置,并将解析结果注册到配置注册表。
+///
+public interface IConfigLoader : IUtility
+{
+ ///
+ /// 执行配置加载并将结果写入注册表。
+ /// 实现应在同一次加载过程中保证注册结果的一致性,避免只加载部分配置后就暴露给运行时消费。
+ ///
+ /// 用于接收配置表的注册表。
+ /// 取消令牌。
+ /// 表示异步加载流程的任务。
+ Task LoadAsync(IConfigRegistry registry, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/Config/IConfigRegistry.cs b/GFramework.Game.Abstractions/Config/IConfigRegistry.cs
new file mode 100644
index 0000000..d40c918
--- /dev/null
+++ b/GFramework.Game.Abstractions/Config/IConfigRegistry.cs
@@ -0,0 +1,75 @@
+using GFramework.Core.Abstractions.Utility;
+
+namespace GFramework.Game.Abstractions.Config;
+
+///
+/// 定义配置注册表契约,用于统一保存和解析按名称注册的配置表。
+/// 注册表是运行时配置系统的入口,负责在加载阶段收集配置表,并在消费阶段提供类型安全查询。
+///
+public interface IConfigRegistry : IUtility
+{
+ ///
+ /// 获取当前已注册配置表数量。
+ ///
+ int Count { get; }
+
+ ///
+ /// 获取所有已注册配置表名称。
+ ///
+ /// 配置表名称集合。
+ IReadOnlyCollection GetTableNames();
+
+ ///
+ /// 注册指定名称的配置表。
+ /// 若名称已存在,则替换旧表,以便开发期热重载使用同一入口刷新配置。
+ ///
+ /// 配置表主键类型。
+ /// 配置项值类型。
+ /// 配置表名称。
+ /// 要注册的配置表实例。
+ void RegisterTable(string name, IConfigTable table)
+ where TKey : notnull;
+
+ ///
+ /// 获取指定名称的配置表。
+ ///
+ /// 配置表主键类型。
+ /// 配置项值类型。
+ /// 配置表名称。
+ /// 匹配的强类型配置表实例。
+ /// 当配置表名称不存在时抛出。
+ /// 当请求类型与已注册配置表类型不匹配时抛出。
+ IConfigTable GetTable(string name)
+ where TKey : notnull;
+
+ ///
+ /// 尝试获取指定名称的配置表。
+ /// 当名称存在但类型不匹配时返回 false,避免消费端将类型错误误判为加载成功。
+ ///
+ /// 配置表主键类型。
+ /// 配置项值类型。
+ /// 配置表名称。
+ /// 匹配的强类型配置表;未找到或类型不匹配时返回空。
+ /// 找到且类型匹配时返回 true,否则返回 false。
+ bool TryGetTable(string name, out IConfigTable? table)
+ where TKey : notnull;
+
+ ///
+ /// 检查指定名称的配置表是否存在。
+ ///
+ /// 配置表名称。
+ /// 存在时返回 true,否则返回 false。
+ bool HasTable(string name);
+
+ ///
+ /// 移除指定名称的配置表。
+ ///
+ /// 配置表名称。
+ /// 移除成功时返回 true,否则返回 false。
+ bool RemoveTable(string name);
+
+ ///
+ /// 清空所有已注册配置表。
+ ///
+ void Clear();
+}
\ No newline at end of file
diff --git a/GFramework.Game.Abstractions/Config/IConfigTable.cs b/GFramework.Game.Abstractions/Config/IConfigTable.cs
new file mode 100644
index 0000000..1cd781a
--- /dev/null
+++ b/GFramework.Game.Abstractions/Config/IConfigTable.cs
@@ -0,0 +1,65 @@
+using GFramework.Core.Abstractions.Utility;
+
+namespace GFramework.Game.Abstractions.Config;
+
+///
+/// 定义配置表的非泛型公共契约,用于在注册表中保存异构配置表实例。
+/// 该接口只暴露运行时发现和诊断所需的元数据,不提供具体类型访问能力。
+///
+public interface IConfigTable : IUtility
+{
+ ///
+ /// 获取配置表主键类型。
+ ///
+ Type KeyType { get; }
+
+ ///
+ /// 获取配置项值类型。
+ ///
+ Type ValueType { get; }
+
+ ///
+ /// 获取当前配置表中的条目数量。
+ ///
+ int Count { get; }
+}
+
+///
+/// 定义强类型只读配置表契约。
+/// 运行时配置表应通过主键执行只读查询,而不是暴露可变集合接口,
+/// 以保持配置数据在加载完成后的稳定性和可预测性。
+///
+/// 配置表主键类型。
+/// 配置项值类型。
+public interface IConfigTable : IConfigTable
+ where TKey : notnull
+{
+ ///
+ /// 获取指定主键的配置项。
+ ///
+ /// 配置项主键。
+ /// 找到的配置项。
+ /// 当主键不存在时抛出。
+ TValue Get(TKey key);
+
+ ///
+ /// 尝试获取指定主键的配置项。
+ ///
+ /// 配置项主键。
+ /// 找到的配置项;未找到时返回默认值。
+ /// 找到配置项时返回 true,否则返回 false。
+ bool TryGet(TKey key, out TValue? value);
+
+ ///
+ /// 检查指定主键是否存在。
+ ///
+ /// 配置项主键。
+ /// 主键存在时返回 true,否则返回 false。
+ bool ContainsKey(TKey key);
+
+ ///
+ /// 获取配置表中的所有配置项快照。
+ ///
+ /// 只读配置项集合。
+ IReadOnlyCollection All();
+}
\ No newline at end of file
diff --git a/GFramework.Game.Tests/Config/ConfigRegistryTests.cs b/GFramework.Game.Tests/Config/ConfigRegistryTests.cs
new file mode 100644
index 0000000..437e968
--- /dev/null
+++ b/GFramework.Game.Tests/Config/ConfigRegistryTests.cs
@@ -0,0 +1,130 @@
+using GFramework.Game.Abstractions.Config;
+using GFramework.Game.Config;
+
+namespace GFramework.Game.Tests.Config;
+
+///
+/// 验证配置注册表的注册、覆盖和类型检查行为。
+///
+[TestFixture]
+public class ConfigRegistryTests
+{
+ ///
+ /// 验证注册后的配置表可以按名称和类型成功解析。
+ ///
+ [Test]
+ public void RegisterTable_Then_GetTable_Should_Return_Registered_Instance()
+ {
+ var registry = new ConfigRegistry();
+ var table = CreateMonsterTable();
+
+ registry.RegisterTable("monster", table);
+
+ var resolved = registry.GetTable("monster");
+
+ Assert.That(resolved, Is.SameAs(table));
+ }
+
+ ///
+ /// 验证同名注册会覆盖旧表,用于后续热重载场景。
+ ///
+ [Test]
+ public void RegisterTable_Should_Replace_Previous_Table_With_Same_Name()
+ {
+ var registry = new ConfigRegistry();
+ var oldTable = CreateMonsterTable();
+ var newTable = new InMemoryConfigTable(
+ new[]
+ {
+ new MonsterConfigStub(3, "Orc")
+ },
+ static config => config.Id);
+
+ registry.RegisterTable("monster", oldTable);
+ registry.RegisterTable("monster", newTable);
+
+ var resolved = registry.GetTable("monster");
+
+ Assert.That(resolved, Is.SameAs(newTable));
+ Assert.That(resolved.Count, Is.EqualTo(1));
+ }
+
+ ///
+ /// 验证请求类型与实际注册类型不匹配时会抛出异常,避免消费端默默读取错误表。
+ ///
+ [Test]
+ public void GetTable_Should_Throw_When_Requested_Type_Does_Not_Match_Registered_Table()
+ {
+ var registry = new ConfigRegistry();
+ registry.RegisterTable("monster", CreateMonsterTable());
+
+ Assert.Throws(() => registry.GetTable("monster"));
+ }
+
+ ///
+ /// 验证移除和清空操作会更新注册表状态。
+ ///
+ [Test]
+ public void RemoveTable_And_Clear_Should_Update_Registry_State()
+ {
+ var registry = new ConfigRegistry();
+ registry.RegisterTable("monster", CreateMonsterTable());
+ registry.RegisterTable("npc", CreateNpcTable());
+
+ var removed = registry.RemoveTable("monster");
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(removed, Is.True);
+ Assert.That(registry.HasTable("monster"), Is.False);
+ Assert.That(registry.Count, Is.EqualTo(1));
+ });
+
+ registry.Clear();
+
+ Assert.That(registry.Count, Is.EqualTo(0));
+ }
+
+ ///
+ /// 创建怪物配置表测试实例。
+ ///
+ /// 怪物配置表。
+ private static IConfigTable CreateMonsterTable()
+ {
+ return new InMemoryConfigTable(
+ new[]
+ {
+ new MonsterConfigStub(1, "Slime"),
+ new MonsterConfigStub(2, "Goblin")
+ },
+ static config => config.Id);
+ }
+
+ ///
+ /// 创建 NPC 配置表测试实例。
+ ///
+ /// NPC 配置表。
+ private static IConfigTable CreateNpcTable()
+ {
+ return new InMemoryConfigTable(
+ new[]
+ {
+ new NpcConfigStub(Guid.NewGuid(), "Guide")
+ },
+ static config => config.Id);
+ }
+
+ ///
+ /// 用于怪物配置表测试的最小配置类型。
+ ///
+ /// 配置主键。
+ /// 配置名称。
+ private sealed record MonsterConfigStub(int Id, string Name);
+
+ ///
+ /// 用于 NPC 配置表测试的最小配置类型。
+ ///
+ /// 配置主键。
+ /// 配置名称。
+ private sealed record NpcConfigStub(Guid Id, string Name);
+}
\ No newline at end of file
diff --git a/GFramework.Game.Tests/Config/InMemoryConfigTableTests.cs b/GFramework.Game.Tests/Config/InMemoryConfigTableTests.cs
new file mode 100644
index 0000000..0d5a7f4
--- /dev/null
+++ b/GFramework.Game.Tests/Config/InMemoryConfigTableTests.cs
@@ -0,0 +1,72 @@
+using GFramework.Game.Config;
+
+namespace GFramework.Game.Tests.Config;
+
+///
+/// 验证内存配置表的基础只读查询行为。
+///
+[TestFixture]
+public class InMemoryConfigTableTests
+{
+ ///
+ /// 验证已存在主键可以被正确查询。
+ ///
+ [Test]
+ public void Get_Should_Return_Config_When_Key_Exists()
+ {
+ var table = new InMemoryConfigTable(
+ new[]
+ {
+ new MonsterConfigStub(1, "Slime"),
+ new MonsterConfigStub(2, "Goblin")
+ },
+ static config => config.Id);
+
+ var result = table.Get(2);
+
+ Assert.That(result.Name, Is.EqualTo("Goblin"));
+ }
+
+ ///
+ /// 验证重复主键会在加载期被拒绝,避免运行期覆盖旧值。
+ ///
+ [Test]
+ public void Constructor_Should_Throw_When_Duplicate_Key_Is_Detected()
+ {
+ Assert.Throws(() =>
+ new InMemoryConfigTable(
+ new[]
+ {
+ new MonsterConfigStub(1, "Slime"),
+ new MonsterConfigStub(1, "Goblin")
+ },
+ static config => config.Id));
+ }
+
+ ///
+ /// 验证 All 返回的集合包含完整快照。
+ ///
+ [Test]
+ public void All_Should_Return_All_Configs()
+ {
+ var table = new InMemoryConfigTable(
+ new[]
+ {
+ new MonsterConfigStub(1, "Slime"),
+ new MonsterConfigStub(2, "Goblin")
+ },
+ static config => config.Id);
+
+ var all = table.All();
+
+ Assert.That(all, Has.Count.EqualTo(2));
+ Assert.That(all.Select(static config => config.Name), Is.EquivalentTo(new[] { "Slime", "Goblin" }));
+ }
+
+ ///
+ /// 用于配置表测试的最小配置类型。
+ ///
+ /// 配置主键。
+ /// 配置名称。
+ private sealed record MonsterConfigStub(int Id, string Name);
+}
\ No newline at end of file
diff --git a/GFramework.Game/Config/ConfigRegistry.cs b/GFramework.Game/Config/ConfigRegistry.cs
new file mode 100644
index 0000000..db80be1
--- /dev/null
+++ b/GFramework.Game/Config/ConfigRegistry.cs
@@ -0,0 +1,162 @@
+using GFramework.Game.Abstractions.Config;
+
+namespace GFramework.Game.Config;
+
+///
+/// 默认配置注册表实现。
+/// 该类型负责统一管理按名称注册的配置表,并在消费端提供类型安全的解析入口。
+/// 为了支持开发期热重载,注册行为采用覆盖策略而不是拒绝重复名称。
+///
+public sealed class ConfigRegistry : IConfigRegistry
+{
+ private const string NameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace.";
+
+ private readonly ConcurrentDictionary _tables = new(StringComparer.Ordinal);
+
+ ///
+ /// 获取已注册的配置表数量。
+ ///
+ public int Count => _tables.Count;
+
+ ///
+ /// 获取所有已注册配置表的名称集合,按字典序排序。
+ ///
+ /// 返回只读的配置表名称集合。
+ public IReadOnlyCollection GetTableNames()
+ {
+ return _tables.Keys.OrderBy(static key => key, StringComparer.Ordinal).ToArray();
+ }
+
+ ///
+ /// 注册一个配置表到注册表中。
+ /// 如果同名的配置表已存在,则会覆盖原有注册以支持热重载。
+ ///
+ /// 配置表主键的类型,必须为非空类型。
+ /// 配置表值的类型。
+ /// 配置表的注册名称,用于后续查找。
+ /// 要注册的配置表实例。
+ /// 当 为 null、空或仅包含空白字符时抛出。
+ /// 当 为 null 时抛出。
+ public void RegisterTable(string name, IConfigTable table)
+ where TKey : notnull
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
+ }
+
+ ArgumentNullException.ThrowIfNull(table);
+
+ _tables[name] = table;
+ }
+
+ ///
+ /// 根据名称获取已注册的配置表,并进行类型验证。
+ ///
+ /// 期望的主键类型,必须为非空类型。
+ /// 期望的值类型。
+ /// 要查找的配置表名称。
+ /// 返回类型匹配的配置表实例。
+ /// 当 为 null、空或仅包含空白字符时抛出。
+ /// 当指定名称的配置表不存在时抛出。
+ ///
+ /// 当找到的配置表类型与请求的类型不匹配时抛出。
+ ///
+ public IConfigTable GetTable(string name)
+ where TKey : notnull
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
+ }
+
+ if (!_tables.TryGetValue(name, out var table))
+ {
+ throw new KeyNotFoundException($"Config table '{name}' was not found.");
+ }
+
+ if (table is IConfigTable typedTable)
+ {
+ return typedTable;
+ }
+
+ throw new InvalidOperationException(
+ $"Config table '{name}' was registered as '{table.KeyType.Name} -> {table.ValueType.Name}', " +
+ $"but the caller requested '{typeof(TKey).Name} -> {typeof(TValue).Name}'.");
+ }
+
+ ///
+ /// 尝试根据名称获取配置表,操作失败时不会抛出异常。
+ ///
+ /// 期望的主键类型,必须为非空类型。
+ /// 期望的值类型。
+ /// 要查找的配置表名称。
+ ///
+ /// 输出参数,如果查找成功则返回类型匹配的配置表实例,否则为 null。
+ ///
+ /// 如果找到指定名称且类型匹配的配置表则返回 true,否则返回 false。
+ /// 当 为 null、空或仅包含空白字符时抛出。
+ public bool TryGetTable(string name, out IConfigTable? table)
+ where TKey : notnull
+ {
+ table = default;
+
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
+ }
+
+ if (!_tables.TryGetValue(name, out var rawTable))
+ {
+ return false;
+ }
+
+ if (rawTable is not IConfigTable typedTable)
+ {
+ return false;
+ }
+
+ table = typedTable;
+ return true;
+ }
+
+ ///
+ /// 检查指定名称的配置表是否已注册。
+ ///
+ /// 要检查的配置表名称。
+ /// 如果配置表已注册则返回 true,否则返回 false。
+ /// 当 为 null、空或仅包含空白字符时抛出。
+ public bool HasTable(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
+ }
+
+ return _tables.ContainsKey(name);
+ }
+
+ ///
+ /// 从注册表中移除指定名称的配置表。
+ ///
+ /// 要移除的配置表名称。
+ /// 如果配置表存在并被成功移除则返回 true,否则返回 false。
+ /// 当 为 null、空或仅包含空白字符时抛出。
+ public bool RemoveTable(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ throw new ArgumentException(NameCannotBeNullOrWhiteSpaceMessage, nameof(name));
+ }
+
+ return _tables.TryRemove(name, out _);
+ }
+
+ ///
+ /// 清空注册表中的所有配置表。
+ ///
+ public void Clear()
+ {
+ _tables.Clear();
+ }
+}
\ No newline at end of file
diff --git a/GFramework.Game/Config/InMemoryConfigTable.cs b/GFramework.Game/Config/InMemoryConfigTable.cs
new file mode 100644
index 0000000..bf6bd28
--- /dev/null
+++ b/GFramework.Game/Config/InMemoryConfigTable.cs
@@ -0,0 +1,118 @@
+using System.Collections.ObjectModel;
+using GFramework.Game.Abstractions.Config;
+
+namespace GFramework.Game.Config;
+
+///
+/// 基于内存字典的只读配置表实现。
+/// 该实现用于 Runtime MVP 阶段,为加载器和注册表提供稳定的只读查询对象。
+///
+/// 配置表主键类型。
+/// 配置项值类型。
+public sealed class InMemoryConfigTable : IConfigTable
+ where TKey : notnull
+{
+ private readonly IReadOnlyCollection _allValues;
+ private readonly IReadOnlyDictionary _entries;
+
+ ///
+ /// 使用配置项序列和主键选择器创建内存配置表。
+ ///
+ /// 配置项序列。
+ /// 用于提取主键的委托。
+ /// 可选的主键比较器。
+ /// 当 或 为空时抛出。
+ /// 当配置项主键重复时抛出。
+ public InMemoryConfigTable(
+ IEnumerable values,
+ Func keySelector,
+ IEqualityComparer? comparer = null)
+ {
+ ArgumentNullException.ThrowIfNull(values);
+ ArgumentNullException.ThrowIfNull(keySelector);
+
+ var dictionary = new Dictionary(comparer);
+ var allValues = new List();
+
+ foreach (var value in values)
+ {
+ var key = keySelector(value);
+
+ // 配置表必须在加载期拒绝重复主键,否则运行期查询结果将不可预测。
+ if (!dictionary.TryAdd(key, value))
+ {
+ throw new InvalidOperationException(
+ $"Duplicate config key '{key}' was detected for table value type '{typeof(TValue).Name}'.");
+ }
+
+ allValues.Add(value);
+ }
+
+ _entries = new ReadOnlyDictionary(dictionary);
+ _allValues = new ReadOnlyCollection(allValues);
+ }
+
+ ///
+ /// 获取配置表的主键类型。
+ ///
+ public Type KeyType => typeof(TKey);
+
+ ///
+ /// 获取配置表的值类型。
+ ///
+ public Type ValueType => typeof(TValue);
+
+ ///
+ /// 获取配置表中配置项的数量。
+ ///
+ public int Count => _entries.Count;
+
+ ///
+ /// 根据主键获取配置项的值。
+ ///
+ /// 要查找的配置项主键。
+ /// 返回对应主键的配置项值。
+ /// 当指定主键的配置项不存在时抛出。
+ public TValue Get(TKey key)
+ {
+ if (!_entries.TryGetValue(key, out var value))
+ {
+ throw new KeyNotFoundException(
+ $"Config key '{key}' was not found in table '{typeof(TValue).Name}'.");
+ }
+
+ return value;
+ }
+
+ ///
+ /// 尝试根据主键获取配置项的值,操作失败时不会抛出异常。
+ ///
+ /// 要查找的配置项主键。
+ ///
+ /// 输出参数,如果查找成功则返回对应的配置项值,否则为默认值。
+ ///
+ /// 如果找到指定主键的配置项则返回 true,否则返回 false。
+ public bool TryGet(TKey key, out TValue? value)
+ {
+ return _entries.TryGetValue(key, out value);
+ }
+
+ ///
+ /// 检查指定主键的配置项是否存在于配置表中。
+ ///
+ /// 要检查的配置项主键。
+ /// 如果配置项已存在则返回 true,否则返回 false。
+ public bool ContainsKey(TKey key)
+ {
+ return _entries.ContainsKey(key);
+ }
+
+ ///
+ /// 获取配置表中所有配置项的集合。
+ ///
+ /// 返回所有配置项值的只读集合。
+ public IReadOnlyCollection All()
+ {
+ return _allValues;
+ }
+}
\ No newline at end of file
diff --git a/GFramework.csproj b/GFramework.csproj
index 1594e9a..e1d069a 100644
--- a/GFramework.csproj
+++ b/GFramework.csproj
@@ -134,6 +134,7 @@
+