mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 20:38:58 +08:00
fix/review-followups: 修复审查反馈并补充提交流程规则
- 修复 Core、Cqrs、Godot 与 Game 模块中的异常契约、空值校验和线程亲和性问题 - 更新 Settings API 为 ApplyAsync 并同步实现、调用点、测试与中文文档 - 补充 AGENTS.md 中的构建校验、自动提交与分支创建规则 - 整理 Logging、WeakCache 与 Resource 相关实现的行为与文档一致性
This commit is contained in:
parent
3f25ea5624
commit
e3652db030
18
AGENTS.md
18
AGENTS.md
@ -19,6 +19,24 @@ All AI agents and contributors must follow these rules when writing, reviewing,
|
|||||||
- After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the
|
- After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the
|
||||||
shell does not fall back to Linux `/usr/bin/git` later in the same WSL session.
|
shell does not fall back to Linux `/usr/bin/git` later in the same WSL session.
|
||||||
|
|
||||||
|
## Git Workflow Rules
|
||||||
|
|
||||||
|
- Every completed task MUST pass at least one build validation before it is considered done.
|
||||||
|
- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project
|
||||||
|
`dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles.
|
||||||
|
- If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git
|
||||||
|
commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit.
|
||||||
|
- Commit messages MUST use Conventional Commits format: `<type>(<scope>): <summary>`.
|
||||||
|
- The commit `summary` MUST use simplified Chinese and briefly describe the main change.
|
||||||
|
- The commit `body` MUST use unordered list items, and each item MUST start with a verb such as `新增`、`修复`、`优化`、
|
||||||
|
`更新`、`补充`、`重构`.
|
||||||
|
- Each commit body bullet MUST describe one independent change point; avoid repeated or redundant descriptions.
|
||||||
|
- Keep technical terms in English when they are established project terms, such as `API`、`Model`、`System`.
|
||||||
|
- If a new task starts while the current branch is `main`, contributors MUST first try to update local `main` from the
|
||||||
|
remote, then create and switch to a dedicated branch before making substantive changes.
|
||||||
|
- The branch naming rule for a new task branch is `<type>/<topic-or-scope>`, where `<type>` should match the intended
|
||||||
|
Conventional Commit category as closely as practical.
|
||||||
|
|
||||||
## Subagent Usage Rules
|
## Subagent Usage Rules
|
||||||
|
|
||||||
- Use subagents only when the task is complex, the context is likely to grow too large, or the work can be split into
|
- Use subagents only when the task is complex, the context is likely to grow too large, or the work can be split into
|
||||||
|
|||||||
@ -22,11 +22,14 @@ public interface IResourceManager : IUtility
|
|||||||
T? Load<T>(string path) where T : class;
|
T? Load<T>(string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步加载资源
|
/// 异步加载指定路径的资源,并在缓存中对并发加载进行去重。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">资源类型</typeparam>
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
/// <param name="path">资源路径</param>
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
/// <returns>资源实例,如果加载失败返回 null</returns>
|
/// <returns>加载成功返回资源实例;加载失败返回 <see langword="null"/>。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>实现内部可能使用 <c>ConfigureAwait(false)</c>,异步延续不保证回到调用线程。</remarks>
|
||||||
Task<T?> LoadAsync<T>(string path) where T : class;
|
Task<T?> LoadAsync<T>(string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -70,10 +73,14 @@ public interface IResourceManager : IUtility
|
|||||||
void UnregisterLoader<T>() where T : class;
|
void UnregisterLoader<T>() where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预加载资源(加载但不返回)
|
/// 预加载资源到缓存中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">资源类型</typeparam>
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
/// <param name="path">资源路径</param>
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
|
/// <returns>表示预加载流程完成的任务。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>内部委托给 <see cref="LoadAsync{T}(string)"/>,同样不捕获同步上下文。</remarks>
|
||||||
Task PreloadAsync<T>(string path) where T : class;
|
Task PreloadAsync<T>(string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -86,4 +93,4 @@ public interface IResourceManager : IUtility
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="strategy">资源释放策略</param>
|
/// <param name="strategy">资源释放策略</param>
|
||||||
void SetReleaseStrategy(IResourceReleaseStrategy strategy);
|
void SetReleaseStrategy(IResourceReleaseStrategy strategy);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,6 +118,24 @@ public class NumericExtensionsTests
|
|||||||
Assert.Throws<ArgumentException>(() => value.Between(100, 0));
|
Assert.Throws<ArgumentException>(() => value.Between(100, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试Between方法在引用类型参数为null时抛出ArgumentNullException
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Between_Should_Throw_ArgumentNullException_When_Reference_Arguments_Are_Null()
|
||||||
|
{
|
||||||
|
string? value = "m";
|
||||||
|
string? min = "a";
|
||||||
|
string? max = "z";
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(value!, null!, max!));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(value!, min!, null!));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(null!, min!, max!));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试Lerp方法在t为0时返回起始值
|
/// 测试Lerp方法在t为0时返回起始值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -205,4 +223,4 @@ public class NumericExtensionsTests
|
|||||||
// Arrange & Act & Assert
|
// Arrange & Act & Assert
|
||||||
Assert.Throws<DivideByZeroException>(() => 50f.InverseLerp(100f, 100f));
|
Assert.Throws<DivideByZeroException>(() => 50f.InverseLerp(100f, 100f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -319,7 +319,12 @@ public class ResultExtensionsTests
|
|||||||
{
|
{
|
||||||
var result = Result<int>.Succeed(-1);
|
var result = Result<int>.Succeed(-1);
|
||||||
var ensured = result.Ensure(x => x > 0, "Value must be positive");
|
var ensured = result.Ensure(x => x > 0, "Value must be positive");
|
||||||
Assert.That(ensured.Exception.Message, Is.EqualTo("Value must be positive"));
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(ensured.Exception.Message, Does.StartWith("Value must be positive"));
|
||||||
|
Assert.That(ensured.Exception, Is.TypeOf<ArgumentException>());
|
||||||
|
Assert.That(((ArgumentException)ensured.Exception).ParamName, Is.EqualTo("result"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -635,4 +640,4 @@ public class ResultExtensionsTests
|
|||||||
|
|
||||||
Assert.That(result.IsSuccess, Is.True);
|
Assert.That(result.IsSuccess, Is.True);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -429,4 +429,20 @@ public class ResultTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.That(str, Is.EqualTo("Fail(Test error)"));
|
Assert.That(str, Is.EqualTo("Fail(Test error)"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试default(Result)不会在相等性比较、哈希计算和字符串格式化中触发空引用异常
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void Default_Result_Should_Be_Safe_For_Equality_HashCode_And_ToString()
|
||||||
|
{
|
||||||
|
var result = default(Result);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(result.Equals(default(Result)), Is.True);
|
||||||
|
Assert.That(result.GetHashCode(), Is.EqualTo(0));
|
||||||
|
Assert.That(result.ToString(), Is.EqualTo("Fail(null)"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -143,6 +143,35 @@ public class LoggingConfigurationTests
|
|||||||
Assert.That(logger3.IsInfoEnabled(), Is.True);
|
Assert.That(logger3.IsInfoEnabled(), Is.True);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CreateFactory_WithOverlappingLoggerPrefixes_ShouldPreferLongestPrefixMatch()
|
||||||
|
{
|
||||||
|
var json = @"{
|
||||||
|
""minLevel"": ""Info"",
|
||||||
|
""appenders"": [
|
||||||
|
{
|
||||||
|
""type"": ""Console"",
|
||||||
|
""formatter"": ""Default""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
""loggerLevels"": {
|
||||||
|
""GFramework"": ""Warning"",
|
||||||
|
""GFramework.Core"": ""Trace""
|
||||||
|
}
|
||||||
|
}";
|
||||||
|
|
||||||
|
var config = LoggingConfigurationLoader.LoadFromJsonString(json);
|
||||||
|
var factory = LoggingConfigurationLoader.CreateFactory(config);
|
||||||
|
|
||||||
|
var logger = factory.GetLogger("GFramework.Core.Logging");
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(logger.IsTraceEnabled(), Is.True);
|
||||||
|
Assert.That(logger.IsDebugEnabled(), Is.True);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void CreateFactory_WithInvalidAppenderType_ShouldThrowException()
|
public void CreateFactory_WithInvalidAppenderType_ShouldThrowException()
|
||||||
{
|
{
|
||||||
@ -308,4 +337,4 @@ public class LoggingConfigurationTests
|
|||||||
Assert.That(config.Appenders[2].MaxFileSize, Is.EqualTo(10485760));
|
Assert.That(config.Appenders[2].MaxFileSize, Is.EqualTo(10485760));
|
||||||
Assert.That(config.LoggerLevels.Count, Is.EqualTo(3));
|
Assert.That(config.LoggerLevels.Count, Is.EqualTo(3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,6 @@ using GFramework.Core.Abstractions.Coroutine;
|
|||||||
|
|
||||||
namespace GFramework.Core.Coroutine.Instructions;
|
namespace GFramework.Core.Coroutine.Instructions;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 等待Task完成的等待指令
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 等待Task完成的等待指令
|
/// 等待Task完成的等待指令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -25,6 +25,10 @@ public static class NumericExtensions
|
|||||||
/// </example>
|
/// </example>
|
||||||
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
|
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
ArgumentNullException.ThrowIfNull(min);
|
||||||
|
ArgumentNullException.ThrowIfNull(max);
|
||||||
|
|
||||||
if (min.CompareTo(max) > 0)
|
if (min.CompareTo(max) > 0)
|
||||||
throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min));
|
throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min));
|
||||||
|
|
||||||
|
|||||||
@ -125,7 +125,10 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
if (_isSuccess)
|
if (_isSuccess)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return _exception!.GetType() == other._exception!.GetType() &&
|
if (_exception is null || other._exception is null)
|
||||||
|
return _exception is null && other._exception is null;
|
||||||
|
|
||||||
|
return _exception.GetType() == other._exception.GetType() &&
|
||||||
string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal);
|
string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,7 +147,12 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
[Pure]
|
[Pure]
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
{
|
{
|
||||||
return _isSuccess ? 1 : HashCode.Combine(_exception!.GetType(), _exception.Message);
|
if (_isSuccess)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return _exception is null
|
||||||
|
? 0
|
||||||
|
: HashCode.Combine(_exception.GetType(), _exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -171,7 +179,7 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
/// <returns>Result 的字符串表示</returns>
|
/// <returns>Result 的字符串表示</returns>
|
||||||
[Pure]
|
[Pure]
|
||||||
public override string ToString() =>
|
public override string ToString() =>
|
||||||
_isSuccess ? "Success" : $"Fail({_exception!.Message})";
|
_isSuccess ? "Success" : (_exception != null ? $"Fail({_exception.Message})" : "Fail(null)");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 尝试执行一个无返回值的操作,并根据执行结果返回成功或失败的 Result
|
/// 尝试执行一个无返回值的操作,并根据执行结果返回成功或失败的 Result
|
||||||
|
|||||||
@ -177,7 +177,7 @@ public static class ResultExtensions
|
|||||||
return result.Match(
|
return result.Match(
|
||||||
succ: value => predicate(value)
|
succ: value => predicate(value)
|
||||||
? result
|
? result
|
||||||
: Result<T>.Fail(new ArgumentException(errorMessage)),
|
: Result<T>.Fail(new ArgumentException(errorMessage, nameof(result))),
|
||||||
fail: _ => result
|
fail: _ => result
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
|
||||||
|
|
||||||
namespace GFramework.Core.Logging;
|
namespace GFramework.Core.Logging;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -10,7 +10,7 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable
|
|||||||
{
|
{
|
||||||
private readonly ILogAppender[] _appenders;
|
private readonly ILogAppender[] _appenders;
|
||||||
private readonly LoggingConfiguration _config;
|
private readonly LoggingConfiguration _config;
|
||||||
private bool _disposed;
|
private int _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化一个基于日志配置创建输出管线的工厂实例。
|
/// 初始化一个基于日志配置创建输出管线的工厂实例。
|
||||||
@ -27,7 +27,7 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -40,7 +40,6 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -52,15 +51,24 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable
|
|||||||
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
||||||
{
|
{
|
||||||
var effectiveLevel = _config.MinLevel;
|
var effectiveLevel = _config.MinLevel;
|
||||||
|
var bestMatchLength = -1;
|
||||||
|
|
||||||
foreach (var kvp in _config.LoggerLevels)
|
foreach (var kvp in _config.LoggerLevels)
|
||||||
{
|
{
|
||||||
if (string.Equals(name, kvp.Key, StringComparison.Ordinal) ||
|
var isExactMatch = string.Equals(name, kvp.Key, StringComparison.Ordinal);
|
||||||
name.StartsWith(kvp.Key + ".", StringComparison.Ordinal))
|
if (isExactMatch)
|
||||||
{
|
{
|
||||||
effectiveLevel = kvp.Value;
|
effectiveLevel = kvp.Value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isPrefixMatch = name.StartsWith(kvp.Key + ".", StringComparison.Ordinal);
|
||||||
|
if (isPrefixMatch && kvp.Key.Length > bestMatchLength)
|
||||||
|
{
|
||||||
|
// 多个命名空间前缀都能命中时,最长前缀代表最具体的配置。
|
||||||
|
bestMatchLength = kvp.Key.Length;
|
||||||
|
effectiveLevel = kvp.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_appenders.Length == 0)
|
if (_appenders.Length == 0)
|
||||||
|
|||||||
@ -69,7 +69,10 @@ public sealed class SamplingFilter : ILogFilter
|
|||||||
|
|
||||||
foreach (var kvp in _samplingStates)
|
foreach (var kvp in _samplingStates)
|
||||||
{
|
{
|
||||||
if (string.Equals(kvp.Key, "*", StringComparison.Ordinal)) continue; // 不清理共享状态
|
if (string.Equals(kvp.Key, "*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue; // 不清理共享状态
|
||||||
|
}
|
||||||
|
|
||||||
if (kvp.Value.IsStale(now, staleThreshold))
|
if (kvp.Value.IsStale(now, staleThreshold))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -82,8 +82,14 @@ public class ResourceManager : IResourceManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步加载资源
|
/// 异步加载指定路径的资源,并在缓存中进行并发去重。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
|
/// <returns>加载成功返回资源实例;加载失败返回 <see langword="null"/>。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>内部使用 <c>ConfigureAwait(false)</c>,后续延续不保证回到调用线程。</remarks>
|
||||||
public async Task<T?> LoadAsync<T>(string path) where T : class
|
public async Task<T?> LoadAsync<T>(string path) where T : class
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
@ -227,8 +233,14 @@ public class ResourceManager : IResourceManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预加载资源
|
/// 预加载资源到缓存中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
|
/// <returns>表示预加载流程完成的任务。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>内部委托给 <see cref="LoadAsync{T}(string)"/>,同样不捕获同步上下文。</remarks>
|
||||||
public async Task PreloadAsync<T>(string path) where T : class
|
public async Task PreloadAsync<T>(string path) where T : class
|
||||||
{
|
{
|
||||||
await LoadAsync<T>(path).ConfigureAwait(false);
|
await LoadAsync<T>(path).ConfigureAwait(false);
|
||||||
|
|||||||
@ -25,10 +25,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
/// <param name="key">缓存键。</param>
|
/// <param name="key">缓存键。</param>
|
||||||
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
|
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
|
||||||
/// <returns>已存在或新创建的缓存值。</returns>
|
/// <returns>已存在或新创建的缓存值。</returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException"><paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。</exception>
|
||||||
/// <paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。
|
/// <exception cref="InvalidOperationException"><paramref name="valueFactory" /> 返回 <see langword="null" />。</exception>
|
||||||
/// 或 <paramref name="valueFactory" /> 返回 <see langword="null" />。
|
|
||||||
/// </exception>
|
|
||||||
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
|
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(key);
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
@ -59,10 +57,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
/// <param name="state">创建缓存值时复用的附加状态。</param>
|
/// <param name="state">创建缓存值时复用的附加状态。</param>
|
||||||
/// <param name="valueFactory">基于键与附加状态创建缓存值的工厂方法。</param>
|
/// <param name="valueFactory">基于键与附加状态创建缓存值的工厂方法。</param>
|
||||||
/// <returns>已存在或新创建的缓存值。</returns>
|
/// <returns>已存在或新创建的缓存值。</returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException"><paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。</exception>
|
||||||
/// <paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。
|
/// <exception cref="InvalidOperationException"><paramref name="valueFactory" /> 返回 <see langword="null" />。</exception>
|
||||||
/// 或 <paramref name="valueFactory" /> 返回 <see langword="null" />。
|
|
||||||
/// </exception>
|
|
||||||
public TValue GetOrAdd<TState>(TKey key, TState state, Func<TKey, TState, TValue> valueFactory)
|
public TValue GetOrAdd<TState>(TKey key, TState state, Func<TKey, TState, TValue> valueFactory)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(key);
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
|
|||||||
@ -8,6 +8,8 @@ namespace GFramework.Cqrs.Internal;
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用,
|
/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用,
|
||||||
/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。
|
/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。
|
||||||
|
/// 线程安全:该类型支持并发访问,读写与清理由底层弱键缓存实现保证一致性。
|
||||||
|
/// 失败模式:键被 GC 回收或调用 <see cref="Clear" /> 后,后续读取可能未命中并触发重建。
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
internal sealed class WeakTypePairCache<TValue>
|
internal sealed class WeakTypePairCache<TValue>
|
||||||
where TValue : class
|
where TValue : class
|
||||||
@ -25,6 +27,7 @@ internal sealed class WeakTypePairCache<TValue>
|
|||||||
/// <paramref name="primaryType" />、<paramref name="secondaryType" /> 或
|
/// <paramref name="primaryType" />、<paramref name="secondaryType" /> 或
|
||||||
/// <paramref name="valueFactory" /> 为 <see langword="null" />。
|
/// <paramref name="valueFactory" /> 为 <see langword="null" />。
|
||||||
/// </exception>
|
/// </exception>
|
||||||
|
/// <exception cref="InvalidOperationException"><paramref name="valueFactory" /> 返回 <see langword="null" />。</exception>
|
||||||
public TValue GetOrAdd(Type primaryType, Type secondaryType, Func<Type, Type, TValue> valueFactory)
|
public TValue GetOrAdd(Type primaryType, Type secondaryType, Func<Type, Type, TValue> valueFactory)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(primaryType);
|
ArgumentNullException.ThrowIfNull(primaryType);
|
||||||
|
|||||||
@ -6,7 +6,11 @@
|
|||||||
public interface IApplyAbleSettings : ISettingsSection
|
public interface IApplyAbleSettings : ISettingsSection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用当前设置到系统中
|
/// 异步应用当前设置到目标系统中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task Apply();
|
/// <returns>
|
||||||
}
|
/// 表示应用流程完成的任务。
|
||||||
|
/// 对于仅执行同步引擎调用的实现,可以返回已完成任务。
|
||||||
|
/// </returns>
|
||||||
|
Task ApplyAsync();
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ namespace GFramework.Game.Tests.Setting;
|
|||||||
public sealed class GodotLocalizationSettingsTests
|
public sealed class GodotLocalizationSettingsTests
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Apply_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage()
|
public async Task ApplyAsync_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage()
|
||||||
{
|
{
|
||||||
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
||||||
manager.Setup(it => it.SetLanguage("eng"));
|
manager.Setup(it => it.SetLanguage("eng"));
|
||||||
@ -21,14 +21,14 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
|
|
||||||
var applicator = CreateApplicator("English", manager.Object, locale => appliedLocale = locale);
|
var applicator = CreateApplicator("English", manager.Object, locale => appliedLocale = locale);
|
||||||
|
|
||||||
await applicator.Apply();
|
await applicator.ApplyAsync();
|
||||||
|
|
||||||
Assert.That(appliedLocale, Is.EqualTo("en"));
|
Assert.That(appliedLocale, Is.EqualTo("en"));
|
||||||
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Apply_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage()
|
public async Task ApplyAsync_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage()
|
||||||
{
|
{
|
||||||
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
||||||
manager.Setup(it => it.SetLanguage("zhs"));
|
manager.Setup(it => it.SetLanguage("zhs"));
|
||||||
@ -36,14 +36,14 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
|
|
||||||
var applicator = CreateApplicator("简体中文", manager.Object, locale => appliedLocale = locale);
|
var applicator = CreateApplicator("简体中文", manager.Object, locale => appliedLocale = locale);
|
||||||
|
|
||||||
await applicator.Apply();
|
await applicator.ApplyAsync();
|
||||||
|
|
||||||
Assert.That(appliedLocale, Is.EqualTo("zh_CN"));
|
Assert.That(appliedLocale, Is.EqualTo("zh_CN"));
|
||||||
manager.Verify(it => it.SetLanguage("zhs"), Times.Once);
|
manager.Verify(it => it.SetLanguage("zhs"), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Apply_ShouldFallbackUnknownLanguageToEnglish()
|
public async Task ApplyAsync_ShouldFallbackUnknownLanguageToEnglish()
|
||||||
{
|
{
|
||||||
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
||||||
manager.Setup(it => it.SetLanguage("eng"));
|
manager.Setup(it => it.SetLanguage("eng"));
|
||||||
@ -51,7 +51,7 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
|
|
||||||
var applicator = CreateApplicator("Esperanto", manager.Object, locale => appliedLocale = locale);
|
var applicator = CreateApplicator("Esperanto", manager.Object, locale => appliedLocale = locale);
|
||||||
|
|
||||||
await applicator.Apply();
|
await applicator.ApplyAsync();
|
||||||
|
|
||||||
Assert.That(appliedLocale, Is.EqualTo("en"));
|
Assert.That(appliedLocale, Is.EqualTo("en"));
|
||||||
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
||||||
@ -71,4 +71,4 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
return new GodotLocalizationSettings(settingsModel.Object, new LocalizationMap(), () => manager,
|
return new GodotLocalizationSettings(settingsModel.Object, new LocalizationMap(), () => manager,
|
||||||
applyGodotLocale);
|
applyGodotLocale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -314,7 +314,7 @@ public sealed class SettingsSystemTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task Apply()
|
public async Task ApplyAsync()
|
||||||
{
|
{
|
||||||
ApplyCount++;
|
ApplyCount++;
|
||||||
|
|
||||||
|
|||||||
@ -198,7 +198,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
foreach (var applicator in _applicators)
|
foreach (var applicator in _applicators)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await applicator.Value.Apply();
|
await applicator.Value.ApplyAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -302,4 +302,4 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,7 +87,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await applyAbleSettings.Apply();
|
await applyAbleSettings.ApplyAsync();
|
||||||
// 发送设置应用成功事件
|
// 发送设置应用成功事件
|
||||||
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, true));
|
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, true));
|
||||||
}
|
}
|
||||||
@ -97,4 +97,4 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
|
|||||||
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, false, ex));
|
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, false, ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,7 +85,7 @@ public abstract class AbstractArchitecture(
|
|||||||
Name = _architectureAnchorName
|
Name = _architectureAnchorName
|
||||||
};
|
};
|
||||||
|
|
||||||
_anchor.Bind(() => _ = DestroyAsync().AsTask());
|
_anchor.Bind(ObserveDestroyAsync);
|
||||||
|
|
||||||
tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor);
|
tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor);
|
||||||
}
|
}
|
||||||
@ -97,16 +97,23 @@ public abstract class AbstractArchitecture(
|
|||||||
/// <typeparam name="TModule">模块类型,必须实现IGodotModule接口</typeparam>
|
/// <typeparam name="TModule">模块类型,必须实现IGodotModule接口</typeparam>
|
||||||
/// <param name="module">要安装的模块实例</param>
|
/// <param name="module">要安装的模块实例</param>
|
||||||
/// <returns>异步任务</returns>
|
/// <returns>异步任务</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="module" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当架构锚点尚未初始化时抛出。</exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该方法会等待锚点进入场景树后再继续执行附加回调,避免模块在非主线程或未就绪状态下访问 Godot 节点 API。
|
||||||
|
/// </remarks>
|
||||||
protected async Task InstallGodotModule<TModule>(TModule module) where TModule : IGodotModule
|
protected async Task InstallGodotModule<TModule>(TModule module) where TModule : IGodotModule
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(module);
|
||||||
|
|
||||||
module.Install(this);
|
module.Install(this);
|
||||||
|
|
||||||
// 检查锚点是否已初始化,未初始化则抛出异常
|
// 检查锚点是否已初始化,未初始化则抛出异常
|
||||||
if (_anchor == null)
|
if (_anchor == null)
|
||||||
throw new InvalidOperationException("Anchor not initialized");
|
throw new InvalidOperationException("Anchor not initialized");
|
||||||
|
|
||||||
// 等待锚点准备就绪
|
// 等待锚点准备就绪,并保持 Godot 同步上下文,以便后续附加逻辑安全访问节点 API。
|
||||||
await _anchor.WaitUntilReadyAsync().ConfigureAwait(false);
|
await _anchor.WaitUntilReadyAsync();
|
||||||
|
|
||||||
// 延迟调用将扩展节点添加为锚点的子节点
|
// 延迟调用将扩展节点添加为锚点的子节点
|
||||||
_anchor.CallDeferred(Node.MethodName.AddChild, module.Node);
|
_anchor.CallDeferred(Node.MethodName.AddChild, module.Node);
|
||||||
@ -138,4 +145,32 @@ public abstract class AbstractArchitecture(
|
|||||||
|
|
||||||
await base.DestroyAsync().ConfigureAwait(false);
|
await base.DestroyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 观察架构异步销毁流程,确保退出树时触发的 fire-and-forget 清理失败可见。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Godot 的 <see cref="Node._ExitTree" /> 回调是同步入口,无法直接等待异步销毁完成;
|
||||||
|
/// 因此这里显式附加错误观察器,把异常写入 Godot 错误输出,避免未观测任务异常被静默吞掉。
|
||||||
|
/// </remarks>
|
||||||
|
private void ObserveDestroyAsync()
|
||||||
|
{
|
||||||
|
_ = ObserveDestroyCoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行并观察异步销毁流程。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示观察任务本身完成的任务。</returns>
|
||||||
|
private async Task ObserveDestroyCoreAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DestroyAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
GD.PushError($"Architecture destruction failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -156,8 +156,28 @@ internal sealed class GodotYamlConfigEnvironment
|
|||||||
|
|
||||||
private static byte[] ReadAllBytesCore(string path)
|
private static byte[] ReadAllBytesCore(string path)
|
||||||
{
|
{
|
||||||
return path.IsGodotPath()
|
if (!path.IsGodotPath())
|
||||||
? FileAccess.GetFileAsBytes(path)
|
{
|
||||||
: File.ReadAllBytes(path);
|
return File.ReadAllBytes(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = FileAccess.GetFileAsBytes(path);
|
||||||
|
var error = FileAccess.GetOpenError();
|
||||||
|
if (error == Error.Ok)
|
||||||
|
{
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw CreateReadException(path, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Exception CreateReadException(string path, Error error)
|
||||||
|
{
|
||||||
|
return error switch
|
||||||
|
{
|
||||||
|
Error.FileNotFound => new FileNotFoundException($"Godot file not found: {path}", path),
|
||||||
|
Error.FileCantOpen => new IOException($"Godot could not open file '{path}'. Error: {error}"),
|
||||||
|
_ => new IOException($"Godot failed to read file '{path}'. Error: {error}")
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -144,7 +144,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
public virtual async ValueTask OnPauseAsync()
|
public virtual async ValueTask OnPauseAsync()
|
||||||
{
|
{
|
||||||
if (_scene != null)
|
if (_scene != null)
|
||||||
await _scene.OnPauseAsync().ConfigureAwait(false);
|
await _scene.OnPauseAsync();
|
||||||
|
|
||||||
// 暂停处理
|
// 暂停处理
|
||||||
Owner.SetProcess(false);
|
Owner.SetProcess(false);
|
||||||
@ -165,7 +165,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
if (_scene != null)
|
if (_scene != null)
|
||||||
await _scene.OnResumeAsync().ConfigureAwait(false);
|
await _scene.OnResumeAsync();
|
||||||
|
|
||||||
// 恢复处理
|
// 恢复处理
|
||||||
Owner.SetProcess(true);
|
Owner.SetProcess(true);
|
||||||
@ -198,7 +198,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
public virtual async ValueTask OnUnloadAsync()
|
public virtual async ValueTask OnUnloadAsync()
|
||||||
{
|
{
|
||||||
if (_scene != null)
|
if (_scene != null)
|
||||||
await _scene.OnUnloadAsync().ConfigureAwait(false);
|
await _scene.OnUnloadAsync();
|
||||||
|
|
||||||
// 释放节点
|
// 释放节点
|
||||||
Owner.QueueFreeX();
|
Owner.QueueFreeX();
|
||||||
|
|||||||
@ -16,8 +16,8 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用音频设置到Godot音频系统
|
/// 应用音频设置到Godot音频系统
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>表示异步操作的任务</returns>
|
/// <returns>已完成的任务;该实现只执行同步音频总线更新。</returns>
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = model.GetData<AudioSettings>();
|
var settings = model.GetData<AudioSettings>();
|
||||||
SetBus(audioBusMap.Master, settings.MasterVolume);
|
SetBus(audioBusMap.Master, settings.MasterVolume);
|
||||||
@ -67,4 +67,4 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap)
|
|||||||
Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f))
|
Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,10 @@ namespace GFramework.Godot.Setting;
|
|||||||
public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings
|
public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用图形设置到Godot引擎
|
/// 将当前图形设置同步应用到 Godot 引擎。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>异步任务</returns>
|
/// <returns>已完成的任务;该实现只执行同步引擎调用,不会启动后台异步工作。</returns>
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = model.GetData<GraphicsSettings>();
|
var settings = model.GetData<GraphicsSettings>();
|
||||||
// 创建分辨率向量
|
// 创建分辨率向量
|
||||||
|
|||||||
@ -77,8 +77,8 @@ public class GodotLocalizationSettings : IResetApplyAbleSettings
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用本地化设置到 Godot 引擎与 GFramework 本地化管理器。
|
/// 应用本地化设置到 Godot 引擎与 GFramework 本地化管理器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>完成的任务</returns>
|
/// <returns>已完成的任务;该实现通过同步 API 推进语言切换。</returns>
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = _model.GetData<LocalizationSettings>();
|
var settings = _model.GetData<LocalizationSettings>();
|
||||||
var locale = _localizationMap.ResolveGodotLocale(settings.Language);
|
var locale = _localizationMap.ResolveGodotLocale(settings.Language);
|
||||||
@ -122,4 +122,4 @@ public class GodotLocalizationSettings : IResetApplyAbleSettings
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ public static class UiPageBehaviorFactory
|
|||||||
/// <param name="key">UI 标识键</param>
|
/// <param name="key">UI 标识键</param>
|
||||||
/// <param name="layer">目标层级</param>
|
/// <param name="layer">目标层级</param>
|
||||||
/// <returns>对应层级的 IUiPageBehavior 实例</returns>
|
/// <returns>对应层级的 IUiPageBehavior 实例</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="layer"/> 不是受支持的 UI 层级时抛出。</exception>
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||||
where T : CanvasItem
|
where T : CanvasItem
|
||||||
{
|
{
|
||||||
|
|||||||
@ -72,7 +72,7 @@ public interface ISettingsModel : IModel
|
|||||||
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
|
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
|
||||||
- `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
|
- `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
|
||||||
- `SaveAllAsync()` 持久化当前所有设置数据
|
- `SaveAllAsync()` 持久化当前所有设置数据
|
||||||
- `ApplyAllAsync()` 依次调用所有 applicator 的 `Apply()`
|
- `ApplyAllAsync()` 依次调用所有 applicator 的 `ApplyAsync()`
|
||||||
|
|
||||||
## SettingsSystem
|
## SettingsSystem
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ public sealed class GameplaySettingsApplicator : IResetApplyAbleSettings
|
|||||||
Data.Reset();
|
Data.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = (GameplaySettings)Data;
|
var settings = (GameplaySettings)Data;
|
||||||
TimeScale.Current = settings.GameSpeed;
|
TimeScale.Current = settings.GameSpeed;
|
||||||
|
|||||||
@ -44,7 +44,7 @@ GodotAudioSettings (Godot 特定实现) → IApplyAbleSettings (可应用设置
|
|||||||
**功能:**
|
**功能:**
|
||||||
|
|
||||||
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
|
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
|
||||||
- 实现 Apply() 方法,将音量设置应用到指定音频总线
|
- 实现 `ApplyAsync()` 方法,将音量设置应用到指定音频总线
|
||||||
- 支持自定义音频总线映射
|
- 支持自定义音频总线映射
|
||||||
- 自动处理音量格式转换(线性值到分贝)
|
- 自动处理音量格式转换(线性值到分贝)
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ graph TD
|
|||||||
F --> L[TranslationServer API]
|
F --> L[TranslationServer API]
|
||||||
F --> M[ILocalizationManager]
|
F --> M[ILocalizationManager]
|
||||||
|
|
||||||
N[SettingsSystem] --> O[Apply Method]
|
N[SettingsSystem] --> O[ApplyAsync Method]
|
||||||
O --> B
|
O --> B
|
||||||
O --> D
|
O --> D
|
||||||
O --> F
|
O --> F
|
||||||
@ -131,7 +131,7 @@ var settings = new AudioSettings
|
|||||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
||||||
|
|
||||||
// 应用设置
|
// 应用设置
|
||||||
audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 自定义音频总线映射
|
#### 自定义音频总线映射
|
||||||
@ -155,7 +155,7 @@ var settings = new AudioSettings
|
|||||||
|
|
||||||
// 使用自定义总线映射应用设置
|
// 使用自定义总线映射应用设置
|
||||||
var audioSettings = new GodotAudioSettings(settings, customBusMap);
|
var audioSettings = new GodotAudioSettings(settings, customBusMap);
|
||||||
await audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 通过设置系统使用
|
#### 通过设置系统使用
|
||||||
@ -170,7 +170,7 @@ audioSettingsData.SfxVolume = 0.9f;
|
|||||||
|
|
||||||
// 创建 Godot 音频设置应用器
|
// 创建 Godot 音频设置应用器
|
||||||
var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap());
|
var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap());
|
||||||
await godotAudioSettings.Apply();
|
await godotAudioSettings.ApplyAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
### 图形设置配置
|
### 图形设置配置
|
||||||
@ -187,7 +187,7 @@ var graphicsSettings = new GodotGraphicsSettings
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 应用设置
|
// 应用设置
|
||||||
await graphicsSettings.Apply();
|
await graphicsSettings.ApplyAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 窗口模式切换
|
#### 窗口模式切换
|
||||||
@ -205,7 +205,7 @@ public class DisplayManager : Node
|
|||||||
public async Task ToggleFullscreen()
|
public async Task ToggleFullscreen()
|
||||||
{
|
{
|
||||||
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
|
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
|
||||||
await _graphicsSettings.Apply();
|
await _graphicsSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetResolution(int width, int height)
|
public async Task SetResolution(int width, int height)
|
||||||
@ -213,7 +213,7 @@ public class DisplayManager : Node
|
|||||||
_graphicsSettings.ResolutionWidth = width;
|
_graphicsSettings.ResolutionWidth = width;
|
||||||
_graphicsSettings.ResolutionHeight = height;
|
_graphicsSettings.ResolutionHeight = height;
|
||||||
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
|
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
|
||||||
await _graphicsSettings.Apply();
|
await _graphicsSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -237,7 +237,7 @@ public class ResolutionPresets
|
|||||||
settings.ResolutionWidth = width;
|
settings.ResolutionWidth = width;
|
||||||
settings.ResolutionHeight = height;
|
settings.ResolutionHeight = height;
|
||||||
settings.Fullscreen = false;
|
settings.Fullscreen = false;
|
||||||
await settings.Apply();
|
await settings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -266,7 +266,7 @@ public sealed class AudioBusMap
|
|||||||
```csharp
|
```csharp
|
||||||
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
|
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
|
||||||
{
|
{
|
||||||
public Task Apply();
|
public Task ApplyAsync();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -278,7 +278,7 @@ public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IA
|
|||||||
**Apply 方法实现:**
|
**Apply 方法实现:**
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
SetBus(busMap.Master, settings.MasterVolume);
|
SetBus(busMap.Master, settings.MasterVolume);
|
||||||
SetBus(busMap.Bgm, settings.BgmVolume);
|
SetBus(busMap.Bgm, settings.BgmVolume);
|
||||||
@ -292,7 +292,7 @@ public Task Apply()
|
|||||||
```csharp
|
```csharp
|
||||||
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
|
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
|
||||||
{
|
{
|
||||||
public Task Apply();
|
public Task ApplyAsync();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -382,7 +382,7 @@ public class AudioManager : Node
|
|||||||
{
|
{
|
||||||
var settings = new AudioSettings { MasterVolume = linearVolume };
|
var settings = new AudioSettings { MasterVolume = linearVolume };
|
||||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
||||||
audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -423,7 +423,7 @@ public class CustomAudioManager : Node
|
|||||||
{
|
{
|
||||||
var audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
|
var audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
|
||||||
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
|
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
|
||||||
audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -522,7 +522,7 @@ public class GraphicsSettingsManager : Node
|
|||||||
|
|
||||||
public async Task ApplyAndSave()
|
public async Task ApplyAndSave()
|
||||||
{
|
{
|
||||||
await _settings.Apply();
|
await _settings.ApplyAsync();
|
||||||
SaveSettings();
|
SaveSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user