Compare commits

...

12 Commits

Author SHA1 Message Date
gewuyou
63b1d71a0e
Merge pull request #127 from GeWuYou/feat/async-log-appender-error-handler
feat(logging): 添加异步日志输出器的错误处理回调功能
2026-03-21 22:09:06 +08:00
GeWuYou
cdc49c319a feat(logging): 添加异步日志输出器功能
- 使用 Channel 实现异步日志处理机制
- 解耦调用线程与慢速日志目标
- 添加全局 Channels 命名空间引用
- 完善日志组件的异步处理能力
2026-03-21 22:04:09 +08:00
GeWuYou
d94d8deb29 fix(logging): 修复异步日志追加器中的操作取消异常处理
- 添加对 OperationCanceledException 的特殊处理,避免将其报告为后台处理错误
- 在 ReportProcessingError 方法中检查并过滤掉操作取消异常
- 添加单元测试验证当内部追加器抛出 OperationCanceledException 时不报告错误
- 创建 CancellationAppender 测试辅助类来模拟取消异常场景
- 确保取消相关的异常不会触发错误处理逻辑
2026-03-21 21:59:51 +08:00
GeWuYou
49609d3821 feat(logging): 添加异步日志输出器的错误处理回调功能
- 在 AsyncLogAppender 构造函数中添加 processingErrorHandler 参数用于处理后台异常
- 实现 ReportProcessingError 方法安全上报后台处理异常而不影响处理循环
- 更新文档注释说明异常处理机制和错误回调用途
- 修改测试用例验证异常处理回调功能的正确性
- 确保错误观察者异常不会终止日志处理线程
- 移除直接写入控制台错误输出的逻辑改为统一回调处理
2026-03-21 21:54:24 +08:00
gewuyou
003fe42ad8
Merge pull request #126 from GeWuYou/refactor/generators-downgrade-to-netstandard20
refactor(generators): 将源代码生成器项目目标框架降级至 netstandard2.0
2026-03-21 21:40:43 +08:00
GeWuYou
a42ec0c282 fix(generator): 修复优先级生成器中的部分关键字检查逻辑
- 将语法节点的部分关键字检查从 Any 操作改为 All 操作
- 修正了对非部分类的诊断报告条件判断
- 确保只有当所有修饰符都不是部分关键字时才报告错误
2026-03-21 21:31:52 +08:00
GeWuYou
884249649d chore(build): 配置项目构建属性以支持源代码生成器
- 为 GFramework.Godot 项目添加 GodotProjectDir 属性默认值
- 在 GFramework 元包中添加 NoPackageAnalysis 属性配置
- 为不同 .NET 版本添加占位符文件到包输出中
- 确保源代码生成器在标准 SDK 构建中正常运行
2026-03-21 21:30:29 +08:00
GeWuYou
d582dffe40 refactor(generators): 将源代码生成器项目目标框架降级至 netstandard2.0
- 将 GFramework.Godot.SourceGenerators 项目的目标框架从 netstandard2.1 更改为 netstandard2.0
- 将 GFramework.SourceGenerators.Abstractions 项目的目标框架从 netstandard2.1 更改为 netstandard2.0
- 将 GFramework.SourceGenerators 项目的目标框架从 netstandard2.1 更改为 netstandard2.0
- 将 GFramework.SourceGenerators.Common 项目的目标框架从 netstandard2.1 更改为 netstandard2.0
- 从 GFramework.SourceGenerators 项目中移除对 GFramework.Core.Abstractions 的引用
- 从 GFramework.SourceGenerators.Abstractions 项目中移除对 GFramework.Core.Abstractions 的引用
- 更新 PriorityGenerator 中的语法检查逻辑,使用 Any 替代 All 进行 partial 关键字检查
- 更新 PriorityAttribute 文档注释中的 cref 格式为 c 标签
2026-03-21 21:20:32 +08:00
GeWuYou
63a6c2e6f0 refactor(abstractions): 移除重复的Key属性声明
- 从LockInfo结构体移除StructLayout特性
- 从ISceneBehavior接口移除重复的Key属性声明
- 从IUiPageBehavior接口移除重复的Key属性声明
- 简化LockInfo类定义减少不必要的using引用
2026-03-21 21:13:53 +08:00
GeWuYou
f3d45169cd refactor(pause): 将暂停状态变化事件改为标准事件模式
- 将 OnPauseStateChanged 事件从 Action<PauseGroup, bool> 类型改为 EventHandler<PauseStateChangedEventArgs>
- 添加 PauseStateChangedEventArgs 类来封装事件数据
- 更新所有事件处理方法的签名以匹配新的事件参数
- 修改文档中相关的事件处理代码示例
- 在 PauseStackManager 中添加 RaisePauseStateChanged 方法统一处理事件触发
- 更新测试代码以适应新的事件处理方式
2026-03-21 21:13:53 +08:00
GeWuYou
86645d34cb fix(generator): 修复诊断消息中的多余句号
- 移除了 Priority 特性描述中的多余句号
- 移除了 IPrioritized 接口实现跳过警告中的多余句号
- 移除了 partial 类要求错误中的多余句号
- 移除了 Priority 特性值验证错误中的多余句号
- 移除了嵌套类限制错误中的多余句号
- 移除了服务获取建议信息中的多余句号
2026-03-21 20:51:03 +08:00
GeWuYou
ab04f0ace7 docs(diagnostic): 更新诊断消息描述文本
- 为 Priority 特性错误消息添加句号结尾
- 为接口实现警告消息添加句号结尾
- 为 partial 类要求错误消息添加句号结尾
- 为整数值验证错误消息添加句号结尾
- 为嵌套类限制错误消息添加句号结尾
- 为服务获取建议消息添加句号结尾
- 在测试项目配置中添加警告级别设置
2026-03-21 20:51:03 +08:00
30 changed files with 322 additions and 140 deletions

View File

@ -0,0 +1,43 @@
// Copyright (c) 2025 GeWuYou
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
namespace GFramework.Core.Abstractions.Concurrency;
/// <summary>
/// 锁信息(用于调试)
/// </summary>
public readonly struct LockInfo
{
/// <summary>
/// 锁的键。
/// </summary>
public string Key { get; init; }
/// <summary>
/// 当前引用计数。
/// </summary>
public int ReferenceCount { get; init; }
/// <summary>
/// 最后访问时间戳Environment.TickCount64
/// </summary>
public long LastAccessTicks { get; init; }
/// <summary>
/// 等待队列长度(近似值)。
/// 注意:这是一个基于 SemaphoreSlim.CurrentCount 的近似指示器,
/// 当 CurrentCount == 0 时表示锁被持有且可能有等待者,返回 1
/// 否则返回 0。这不是精确的等待者数量仅用于调试参考。
/// </summary>
public int WaitingCount { get; init; }
}

View File

@ -11,11 +11,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
using System.Runtime.InteropServices;
namespace GFramework.Core.Abstractions.Concurrency; namespace GFramework.Core.Abstractions.Concurrency;
/// <summary> /// <summary>
/// 锁统计信息 /// 锁统计信息
/// </summary> /// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly struct LockStatistics public readonly struct LockStatistics
{ {
/// <summary> /// <summary>
@ -37,33 +40,4 @@ public readonly struct LockStatistics
/// 累计清理的锁数量 /// 累计清理的锁数量
/// </summary> /// </summary>
public int TotalCleaned { get; init; } public int TotalCleaned { get; init; }
}
/// <summary>
/// 锁信息(用于调试)
/// </summary>
public readonly struct LockInfo
{
/// <summary>
/// 锁的键
/// </summary>
public string Key { get; init; }
/// <summary>
/// 当前引用计数
/// </summary>
public int ReferenceCount { get; init; }
/// <summary>
/// 最后访问时间戳Environment.TickCount64
/// </summary>
public long LastAccessTicks { get; init; }
/// <summary>
/// 等待队列长度(近似值)
/// 注意:这是一个基于 SemaphoreSlim.CurrentCount 的近似指示器,
/// 当 CurrentCount == 0 时表示锁被持有且可能有等待者,返回 1
/// 否则返回 0。这不是精确的等待者数量仅用于调试参考。
/// </summary>
public int WaitingCount { get; init; }
} }

View File

@ -75,7 +75,9 @@ public interface IPauseStackManager : IContextUtility
void UnregisterHandler(IPauseHandler handler); void UnregisterHandler(IPauseHandler handler);
/// <summary> /// <summary>
/// 暂停状态变化事件 /// 暂停状态变化事件。
/// 事件遵循标准 .NET 事件模式,事件源为触发通知的暂停管理器实例,
/// 事件数据由 <see cref="PauseStateChangedEventArgs"/> 提供。
/// </summary> /// </summary>
event Action<PauseGroup, bool>? OnPauseStateChanged; event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
} }

View File

@ -0,0 +1,30 @@
namespace GFramework.Core.Abstractions.Pause;
/// <summary>
/// 表示暂停状态变化事件的数据。
/// 该类型用于向事件订阅者传递暂停组以及该组变化后的暂停状态。
/// </summary>
public sealed class PauseStateChangedEventArgs : EventArgs
{
/// <summary>
/// 初始化 <see cref="PauseStateChangedEventArgs"/> 的新实例。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">暂停组变化后的新状态。</param>
public PauseStateChangedEventArgs(PauseGroup group, bool isPaused)
{
Group = group;
IsPaused = isPaused;
}
/// <summary>
/// 获取发生状态变化的暂停组。
/// </summary>
public PauseGroup Group { get; }
/// <summary>
/// 获取暂停组变化后的新状态。
/// 为 <see langword="true"/> 表示进入暂停,为 <see langword="false"/> 表示恢复运行。
/// </summary>
public bool IsPaused { get; }
}

View File

@ -7,6 +7,7 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject> <IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Mediator.Abstractions" Version="3.0.1"/> <PackageReference Include="Mediator.Abstractions" Version="3.0.1"/>

View File

@ -1,6 +1,5 @@
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging.Appenders; using GFramework.Core.Logging.Appenders;
using NUnit.Framework;
namespace GFramework.Core.Tests.Logging; namespace GFramework.Core.Tests.Logging;
@ -152,8 +151,12 @@ public class AsyncLogAppenderTests
[Test] [Test]
public void Append_WhenInnerAppenderThrows_ShouldNotCrash() public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
{ {
var reportedExceptions = new List<Exception>();
var innerAppender = new ThrowingAppender(); var innerAppender = new ThrowingAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000); using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: reportedExceptions.Add);
// 即使内部 Appender 抛出异常,也不应该影响调用线程 // 即使内部 Appender 抛出异常,也不应该影响调用线程
Assert.DoesNotThrow(() => Assert.DoesNotThrow(() =>
@ -165,7 +168,56 @@ public class AsyncLogAppenderTests
} }
}); });
Thread.Sleep(100); // 等待后台处理 asyncAppender.Flush();
Assert.That(reportedExceptions, Has.Count.EqualTo(10));
Assert.That(reportedExceptions, Has.All.TypeOf<InvalidOperationException>());
Assert.That(reportedExceptions.Select(static exception => exception.Message),
Has.All.EqualTo("Test exception"));
}
[Test]
public void Append_WhenProcessingErrorHandlerThrows_ShouldStillNotCrash()
{
var innerAppender = new ThrowingAppender();
using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: static _ => throw new InvalidOperationException("Observer failure"));
Assert.DoesNotThrow(() =>
{
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
});
Assert.That(asyncAppender.Flush(), Is.True);
}
[Test]
public void Append_WhenInnerAppenderThrowsOperationCanceledException_ShouldNotReportError()
{
var reportedExceptions = new List<Exception>();
var innerAppender = new CancellationAppender();
using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: reportedExceptions.Add);
Assert.DoesNotThrow(() =>
{
for (int i = 0; i < 10; i++)
{
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", $"Message {i}", null, null);
asyncAppender.Append(entry);
}
});
Assert.That(asyncAppender.Flush(), Is.True);
Assert.That(reportedExceptions, Is.Empty);
} }
// 辅助测试类 // 辅助测试类
@ -228,4 +280,20 @@ public class AsyncLogAppenderTests
{ {
} }
} }
private class CancellationAppender : ILogAppender
{
public void Append(LogEntry entry)
{
throw new OperationCanceledException("Simulated cancellation");
}
public void Flush()
{
}
public void Dispose()
{
}
}
} }

View File

@ -1,6 +1,5 @@
using GFramework.Core.Abstractions.Pause; using GFramework.Core.Abstractions.Pause;
using GFramework.Core.Pause; using GFramework.Core.Pause;
using NUnit.Framework;
namespace GFramework.Core.Tests.Pause; namespace GFramework.Core.Tests.Pause;
@ -220,11 +219,11 @@ public class PauseStackManagerTests
PauseGroup? eventGroup = null; PauseGroup? eventGroup = null;
bool? eventIsPaused = null; bool? eventIsPaused = null;
_manager.OnPauseStateChanged += (group, isPaused) => _manager.OnPauseStateChanged += (_, e) =>
{ {
eventTriggered = true; eventTriggered = true;
eventGroup = group; eventGroup = e.Group;
eventIsPaused = isPaused; eventIsPaused = e.IsPaused;
}; };
_manager.Push("Test", PauseGroup.Gameplay); _manager.Push("Test", PauseGroup.Gameplay);
@ -243,10 +242,10 @@ public class PauseStackManagerTests
var token = _manager.Push("Test"); var token = _manager.Push("Test");
bool eventTriggered = false; bool eventTriggered = false;
_manager.OnPauseStateChanged += (group, isPaused) => _manager.OnPauseStateChanged += (_, e) =>
{ {
eventTriggered = true; eventTriggered = true;
Assert.That(isPaused, Is.False); Assert.That(e.IsPaused, Is.False);
}; };
_manager.Pop(token); _manager.Pop(token);

View File

@ -17,11 +17,6 @@ internal sealed class CoroutineSlot
/// </summary> /// </summary>
public CoroutineHandle Handle; public CoroutineHandle Handle;
/// <summary>
/// 协程是否已经开始执行
/// </summary>
public bool HasStarted;
/// <summary> /// <summary>
/// 协程的优先级 /// 协程的优先级
/// </summary> /// </summary>

View File

@ -208,13 +208,10 @@ public class PriorityEvent<T> : IEvent
MergeAndSortHandlers(T t) MergeAndSortHandlers(T t)
{ {
var (normalSnapshot, contextSnapshot) = CreateSnapshots(); var (normalSnapshot, contextSnapshot) = CreateSnapshots();
// 使用快照避免迭代期间修改 // 使用统一的投影方法显式固定元组的可空标注,避免 LINQ 在 Concat 时推断出不兼容的签名。
return normalSnapshot return normalSnapshot
.Select(h => (h.Priority, Handler: (Action?)(() => h.Handler.Invoke(t)), .Select(h => CreateNormalHandlerInvocation(h, t))
ContextHandler: (Action<EventContext<T>>?)null, IsContext: false)) .Concat(contextSnapshot.Select(CreateContextHandlerInvocation))
.Concat(contextSnapshot
.Select(h => (h.Priority, Handler: (Action?)null,
ContextHandler: (Action<EventContext<T>>?)h.Handler, IsContext: true)))
.OrderByDescending(h => h.Priority) .OrderByDescending(h => h.Priority)
.ToList(); .ToList();
} }
@ -289,6 +286,29 @@ public class PriorityEvent<T> : IEvent
} }
} }
/// <summary>
/// 将普通事件处理器转换为统一的调用描述。
/// </summary>
/// <param name="handler">要包装的普通处理器。</param>
/// <param name="t">当前触发的事件数据。</param>
/// <returns>可与上下文处理器合并排序的统一调用描述。</returns>
private static (int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)
CreateNormalHandlerInvocation(EventHandler handler, T t)
{
return (handler.Priority, () => handler.Handler.Invoke(t), null, false);
}
/// <summary>
/// 将上下文事件处理器转换为统一的调用描述。
/// </summary>
/// <param name="handler">要包装的上下文处理器。</param>
/// <returns>可与普通处理器合并排序的统一调用描述。</returns>
private static (int Priority, Action? Handler, Action<EventContext<T>>? ContextHandler, bool IsContext)
CreateContextHandlerInvocation(ContextEventHandler handler)
{
return (handler.Priority, null, handler.Handler, true);
}
/// <summary> /// <summary>
/// 事件处理器包装类,包含处理器和优先级 /// 事件处理器包装类,包含处理器和优先级
/// </summary> /// </summary>

View File

@ -15,4 +15,5 @@ global using System;
global using System.Collections.Generic; global using System.Collections.Generic;
global using System.Linq; global using System.Linq;
global using System.Threading; global using System.Threading;
global using System.Threading.Tasks; global using System.Threading.Tasks;
global using System.Threading.Channels;

View File

@ -1,17 +1,26 @@
using System.Threading.Channels;
using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Logging.Appenders; namespace GFramework.Core.Logging.Appenders;
/// <summary> /// <summary>
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入 /// 异步日志输出器,使用 <see cref="Channel" /> 将调用线程与慢速日志目标解耦。
/// </summary> /// </summary>
public sealed class AsyncLogAppender : ILogAppender, IDisposable /// <remarks>
/// <para>
/// 该输出器在后台线程中顺序消费日志条目,因此调用方不会因为文件 IO 或其他慢速输出目标而阻塞。
/// </para>
/// <para>
/// 内部输出器抛出的异常不会重新抛回调用线程;如需观察后台处理失败,请在构造函数中提供
/// <c>processingErrorHandler</c> 回调。
/// </para>
/// </remarks>
public sealed class AsyncLogAppender : ILogAppender
{ {
private readonly Channel<LogEntry> _channel; private readonly Channel<LogEntry> _channel;
private readonly CancellationTokenSource _cts; private readonly CancellationTokenSource _cts;
private readonly SemaphoreSlim _flushSemaphore = new(0, 1); private readonly SemaphoreSlim _flushSemaphore = new(0, 1);
private readonly ILogAppender _innerAppender; private readonly ILogAppender _innerAppender;
private readonly Action<Exception>? _processingErrorHandler;
private readonly Task _processingTask; private readonly Task _processingTask;
private bool _disposed; private bool _disposed;
private volatile bool _flushRequested; private volatile bool _flushRequested;
@ -21,9 +30,17 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
/// </summary> /// </summary>
/// <param name="innerAppender">内部日志输出器</param> /// <param name="innerAppender">内部日志输出器</param>
/// <param name="bufferSize">缓冲区大小(默认 10000</param> /// <param name="bufferSize">缓冲区大小(默认 10000</param>
public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000) /// <param name="processingErrorHandler">
/// 后台处理日志时的错误回调。
/// 默认值为 <see langword="null" />,表示吞掉内部异常以避免污染宿主标准错误输出。
/// </param>
public AsyncLogAppender(
ILogAppender innerAppender,
int bufferSize = 10000,
Action<Exception>? processingErrorHandler = null)
{ {
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender)); _innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
_processingErrorHandler = processingErrorHandler;
if (bufferSize <= 0) if (bufferSize <= 0)
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize)); throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
@ -138,7 +155,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
} }
/// <summary> /// <summary>
/// 后台处理日志的异步方法 /// 后台处理日志的异步方法。
/// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。
/// </summary> /// </summary>
private async Task ProcessLogsAsync(CancellationToken cancellationToken) private async Task ProcessLogsAsync(CancellationToken cancellationToken)
{ {
@ -152,8 +170,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
// 记录内部错误到控制台(避免递归) // 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}"); ReportProcessingError(ex);
} }
// 检查是否有刷新请求且通道已空 // 检查是否有刷新请求且通道已空
@ -175,7 +193,7 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}"); ReportProcessingError(ex);
} }
finally finally
{ {
@ -184,10 +202,37 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
{ {
_innerAppender.Flush(); _innerAppender.Flush();
} }
catch catch (Exception ex)
{ {
// 忽略刷新错误 ReportProcessingError(ex);
} }
} }
} }
/// <summary>
/// 上报后台处理异常,同时隔离观察者自身抛出的错误,避免终止处理循环。
/// 取消相关异常表示关闭流程中的预期控制流,不应被视为后台处理失败。
/// </summary>
/// <param name="exception">后台处理中捕获到的异常。</param>
private void ReportProcessingError(Exception exception)
{
if (exception is OperationCanceledException)
{
return;
}
if (_processingErrorHandler is null)
{
return;
}
try
{
_processingErrorHandler(exception);
}
catch
{
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
}
}
} }

View File

@ -80,7 +80,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
// 触发事件 // 触发事件
try try
{ {
OnPauseStateChanged?.Invoke(group, false); RaisePauseStateChanged(group, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -97,7 +97,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
/// <summary> /// <summary>
/// 暂停状态变化事件,当暂停状态发生改变时触发。 /// 暂停状态变化事件,当暂停状态发生改变时触发。
/// </summary> /// </summary>
public event Action<PauseGroup, bool>? OnPauseStateChanged; public event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
/// <summary> /// <summary>
/// 推入一个新的暂停请求到指定的暂停组中。 /// 推入一个新的暂停请求到指定的暂停组中。
@ -488,7 +488,18 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
} }
// 触发事件 // 触发事件
OnPauseStateChanged?.Invoke(group, isPaused); RaisePauseStateChanged(group, isPaused);
}
/// <summary>
/// 以标准事件模式发布暂停状态变化事件。
/// 所有状态变更路径都通过该方法创建统一的事件参数,避免不同调用点出现不一致的载荷。
/// </summary>
/// <param name="group">发生状态变化的暂停组。</param>
/// <param name="isPaused">暂停组变化后的新状态。</param>
private void RaisePauseStateChanged(PauseGroup group, bool isPaused)
{
OnPauseStateChanged?.Invoke(this, new PauseStateChangedEventArgs(group, isPaused));
} }
/// <summary> /// <summary>

View File

@ -21,12 +21,6 @@ namespace GFramework.Game.Abstractions.Scene;
/// </summary> /// </summary>
public interface ISceneBehavior : IRoute public interface ISceneBehavior : IRoute
{ {
/// <summary>
/// 获取场景的唯一标识符。
/// 用于区分不同的场景实例。
/// </summary>
string Key { get; }
/// <summary> /// <summary>
/// 获取场景的原始对象。 /// 获取场景的原始对象。
/// </summary> /// </summary>

View File

@ -31,8 +31,9 @@ public interface ISceneRouteGuard : IRouteGuard<ISceneBehavior>
/// <summary> /// <summary>
/// 异步检查是否允许离开指定场景。 /// 异步检查是否允许离开指定场景。
/// 该成员显式细化了通用路由守卫的离开检查,使场景守卫在 API 文档中保持场景语义。
/// </summary> /// </summary>
/// <param name="sceneKey">当前场景的唯一标识符。</param> /// <param name="sceneKey">当前场景的唯一标识符。</param>
/// <returns>如果允许离开则返回 true否则返回 false。</returns> /// <returns>如果允许离开则返回 true否则返回 false。</returns>
ValueTask<bool> CanLeaveAsync(string sceneKey); new ValueTask<bool> CanLeaveAsync(string sceneKey);
} }

View File

@ -46,14 +46,6 @@ public interface IUiPageBehavior : IRoute
/// <returns>页面视图实例。</returns> /// <returns>页面视图实例。</returns>
object View { get; } object View { get; }
/// <summary>
/// 获取键值
/// </summary>
/// <value>返回当前对象的键标识符</value>
string Key { get; }
/// <summary> /// <summary>
/// 获取页面是否处于活动状态 /// 获取页面是否处于活动状态
/// </summary> /// </summary>

View File

@ -17,9 +17,10 @@ public interface IUiRouteGuard : IRouteGuard<IUiPageBehavior>
ValueTask<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param); ValueTask<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param);
/// <summary> /// <summary>
/// 离开UI前的检查 /// 离开UI前的检查。
/// 该成员显式细化了通用路由守卫的离开检查,使 UI 守卫在 API 文档中保持 UI 语义。
/// </summary> /// </summary>
/// <param name="uiKey">当前UI标识符</param> /// <param name="uiKey">当前UI标识符</param>
/// <returns>true表示允许离开false表示拦截</returns> /// <returns>true表示允许离开false表示拦截</returns>
ValueTask<bool> CanLeaveAsync(string uiKey); new ValueTask<bool> CanLeaveAsync(string uiKey);
} }

View File

@ -1,7 +1,7 @@
<Project> <Project>
<!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build --> <!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -->
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- <!--

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<PackageId>GeWuYou.$(AssemblyName)</PackageId> <PackageId>GeWuYou.$(AssemblyName)</PackageId>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<!-- 这是 Analyzer不是运行时库 --> <!-- 这是 Analyzer不是运行时库 -->
<IsRoslynAnalyzer>true</IsRoslynAnalyzer> <IsRoslynAnalyzer>true</IsRoslynAnalyzer>

View File

@ -6,6 +6,9 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Godot.SourceGenerators expects this property from Godot.NET.Sdk.
Provide a safe default so source generators can run in plain SDK-style builds as well. -->
<GodotProjectDir Condition="'$(GodotProjectDir)' == ''">$(MSBuildProjectDirectory)</GodotProjectDir>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,7 +1,7 @@
namespace GFramework.SourceGenerators.Abstractions.Bases; namespace GFramework.SourceGenerators.Abstractions.Bases;
/// <summary> /// <summary>
/// 标记类的优先级,自动生成 <see cref="GFramework.Core.Abstractions.Bases.IPrioritized"/> 接口实现 /// 标记类的优先级,自动生成 <c>GFramework.Core.Abstractions.Bases.IPrioritized</c> 接口实现。
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// 使用此特性可以避免手动实现 IPrioritized 接口。 /// 使用此特性可以避免手动实现 IPrioritized 接口。

View File

@ -1,7 +1,7 @@
<Project> <Project>
<!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build --> <!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -->
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- <!--

View File

@ -14,9 +14,6 @@
<ItemGroup> <ItemGroup>
<Using Include="GFramework.SourceGenerators.Abstractions"/> <Using Include="GFramework.SourceGenerators.Abstractions"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Update="Meziantou.Analyzer" Version="3.0.25"> <PackageReference Update="Meziantou.Analyzer" Version="3.0.25">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -1,7 +1,7 @@
<Project> <Project>
<!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build --> <!-- import parent: https://docs.microsoft.com/en-us/visualstudio/msbuild/customize-your-build -->
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- <!--

View File

@ -56,7 +56,7 @@ public sealed class PriorityGenerator : MetadataAttributeClassGeneratorBase
} }
// 3. 必须是 partial // 3. 必须是 partial
if (syntax.Modifiers.All(m => m.Kind() != SyntaxKind.PartialKeyword)) if (syntax.Modifiers.All(m => !m.IsKind(SyntaxKind.PartialKeyword)))
{ {
context.ReportDiagnostic(Diagnostic.Create( context.ReportDiagnostic(Diagnostic.Create(
PriorityDiagnostic.MustBePartial, PriorityDiagnostic.MustBePartial,

View File

@ -19,7 +19,7 @@ internal static class PriorityDiagnostic
category: Category, category: Category,
defaultSeverity: DiagnosticSeverity.Error, defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true, isEnabledByDefault: true,
description: "Priority 特性设计用于类级别的优先级标记,不支持其他类型" description: "Priority 特性设计用于类级别的优先级标记,不支持其他类型."
); );
/// <summary> /// <summary>
@ -32,7 +32,7 @@ internal static class PriorityDiagnostic
category: Category, category: Category,
defaultSeverity: DiagnosticSeverity.Warning, defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true, isEnabledByDefault: true,
description: "当类已经手动实现 IPrioritized 接口时,源生成器将跳过代码生成以避免冲突" description: "当类已经手动实现 IPrioritized 接口时,源生成器将跳过代码生成以避免冲突."
); );
/// <summary> /// <summary>
@ -45,7 +45,7 @@ internal static class PriorityDiagnostic
category: Category, category: Category,
defaultSeverity: DiagnosticSeverity.Error, defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true, isEnabledByDefault: true,
description: "源生成器需要在 partial 类中生成 IPrioritized 接口实现" description: "源生成器需要在 partial 类中生成 IPrioritized 接口实现."
); );
/// <summary> /// <summary>
@ -58,7 +58,7 @@ internal static class PriorityDiagnostic
category: Category, category: Category,
defaultSeverity: DiagnosticSeverity.Error, defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true, isEnabledByDefault: true,
description: "Priority 特性必须提供一个有效的整数值" description: "Priority 特性必须提供一个有效的整数值."
); );
/// <summary> /// <summary>
@ -71,7 +71,7 @@ internal static class PriorityDiagnostic
category: Category, category: Category,
defaultSeverity: DiagnosticSeverity.Error, defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true, isEnabledByDefault: true,
description: "Priority 特性仅支持顶层类,不支持嵌套类。请将嵌套类移至命名空间级别。" description: "Priority 特性仅支持顶层类,不支持嵌套类.请将嵌套类移至命名空间级别."
); );
/// <summary> /// <summary>
@ -84,6 +84,6 @@ internal static class PriorityDiagnostic
category: "GFramework.Usage", category: "GFramework.Usage",
defaultSeverity: DiagnosticSeverity.Info, defaultSeverity: DiagnosticSeverity.Info,
isEnabledByDefault: true, isEnabledByDefault: true,
description: "当获取实现了 IPrioritized 接口的服务时,应使用 GetAllByPriority 方法以确保按优先级排序" description: "当获取实现了 IPrioritized 接口的服务时,应使用 GetAllByPriority 方法以确保按优先级排序."
); );
} }

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<PackageId>GeWuYou.$(AssemblyName)</PackageId> <PackageId>GeWuYou.$(AssemblyName)</PackageId>
<TargetFramework>netstandard2.1</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<!-- 这是 Analyzer不是运行时库 --> <!-- 这是 Analyzer不是运行时库 -->
<IsRoslynAnalyzer>true</IsRoslynAnalyzer> <IsRoslynAnalyzer>true</IsRoslynAnalyzer>
@ -30,7 +30,6 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj" PrivateAssets="all"/> <ProjectReference Include="..\$(AssemblyName).Abstractions\$(AssemblyName).Abstractions.csproj" PrivateAssets="all"/>
<ProjectReference Include="..\$(AssemblyName).Common\$(AssemblyName).Common.csproj" PrivateAssets="all"/> <ProjectReference Include="..\$(AssemblyName).Common\$(AssemblyName).Common.csproj" PrivateAssets="all"/>
<ProjectReference Include="..\GFramework.Core.Abstractions\GFramework.Core.Abstractions.csproj" PrivateAssets="all"/>
</ItemGroup> </ItemGroup>
<!-- ★关键:只把 Generator DLL 放进 analyzers --> <!-- ★关键:只把 Generator DLL 放进 analyzers -->

View File

@ -16,11 +16,16 @@
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<IncludeBuildOutput>false</IncludeBuildOutput> <IncludeBuildOutput>false</IncludeBuildOutput>
<!-- This package is a pure meta-package that only aggregates dependencies. -->
<NoPackageAnalysis>false</NoPackageAnalysis>
</PropertyGroup> </PropertyGroup>
<!-- 排除不需要参与打包/编译的目录 --> <!-- 排除不需要参与打包/编译的目录 -->
<ItemGroup> <ItemGroup>
<None Include="README.md" Pack="true" PackagePath=""/> <None Include="README.md" Pack="true" PackagePath=""/>
<None Include="packaging/_._" Pack="true" PackagePath="lib/net8.0/_._"/>
<None Include="packaging/_._" Pack="true" PackagePath="lib/net9.0/_._"/>
<None Include="packaging/_._" Pack="true" PackagePath="lib/net10.0/_._"/>
<None Remove="GFramework.Core\**"/> <None Remove="GFramework.Core\**"/>
<None Remove="GFramework.Game\**"/> <None Remove="GFramework.Game\**"/>
<None Remove="GFramework.Godot\**"/> <None Remove="GFramework.Godot\**"/>

View File

@ -100,8 +100,8 @@ void ClearAll();
void RegisterHandler(IPauseHandler handler); void RegisterHandler(IPauseHandler handler);
void UnregisterHandler(IPauseHandler handler); void UnregisterHandler(IPauseHandler handler);
// 状态变化事件 // 状态变化事件
event Action<PauseGroup, bool>? OnPauseStateChanged; event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
``` ```
## 基本用法 ## 基本用法
@ -377,16 +377,16 @@ public partial class PauseIndicator : IController
_pauseManager.OnPauseStateChanged += OnPauseStateChanged; _pauseManager.OnPauseStateChanged += OnPauseStateChanged;
} }
private void OnPauseStateChanged(PauseGroup group, bool isPaused) private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
{ {
Console.WriteLine($"暂停状态变化: 组={group}, 暂停={isPaused}"); Console.WriteLine($"暂停状态变化: 组={e.Group}, 暂停={e.IsPaused}");
if (group == PauseGroup.Global) if (e.Group == PauseGroup.Global)
{ {
if (isPaused) if (e.IsPaused)
{ {
ShowPauseIndicator(); ShowPauseIndicator();
} }
else else
{ {
HidePauseIndicator(); HidePauseIndicator();
@ -705,7 +705,7 @@ public partial class ProperCleanup : IController
_pauseManager.OnPauseStateChanged -= OnPauseChanged; _pauseManager.OnPauseStateChanged -= OnPauseChanged;
} }
private void OnPauseChanged(PauseGroup group, bool isPaused) { } private void OnPauseChanged(object? sender, PauseStateChangedEventArgs e) { }
} }
``` ```
@ -743,13 +743,13 @@ public partial class PauseMenu : Control
// 方案 2: 监听暂停事件 // 方案 2: 监听暂停事件
var pauseManager = this.GetUtility<IPauseStackManager>(); var pauseManager = this.GetUtility<IPauseStackManager>();
pauseManager.OnPauseStateChanged += (group, isPaused) => pauseManager.OnPauseStateChanged += (_, e) =>
{ {
if (group == PauseGroup.Global) if (e.Group == PauseGroup.Global)
{ {
Visible = isPaused; Visible = e.IsPaused;
} }
}; };
} }
} }
``` ```
@ -887,15 +887,15 @@ public class PauseEventBridge : AbstractSystem
{ {
var pauseManager = this.GetUtility<IPauseStackManager>(); var pauseManager = this.GetUtility<IPauseStackManager>();
pauseManager.OnPauseStateChanged += (group, isPaused) => pauseManager.OnPauseStateChanged += (_, e) =>
{ {
// 发送暂停事件 // 发送暂停事件
this.SendEvent(new GamePausedEvent this.SendEvent(new GamePausedEvent
{ {
Group = group, Group = e.Group,
IsPaused = isPaused IsPaused = e.IsPaused
}); });
}; };
} }
} }

View File

@ -44,7 +44,7 @@ public interface IPauseStackManager : IContextUtility
int GetPauseDepth(PauseGroup group = PauseGroup.Global); int GetPauseDepth(PauseGroup group = PauseGroup.Global);
// 暂停状态变化事件 // 暂停状态变化事件
event Action<PauseGroup, bool>? OnPauseStateChanged; event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
} }
``` ```
@ -477,12 +477,12 @@ public partial class PauseIndicator : Label
pauseManager.OnPauseStateChanged -= OnPauseStateChanged; pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
} }
private void OnPauseStateChanged(PauseGroup group, bool isPaused) private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
{ {
if (group == PauseGroup.Global) if (e.Group == PauseGroup.Global)
{ {
Text = isPaused ? "游戏已暂停" : "游戏运行中"; Text = e.IsPaused ? "游戏已暂停" : "游戏运行中";
Visible = isPaused; Visible = e.IsPaused;
} }
} }
} }
@ -502,16 +502,16 @@ public partial class PauseDebugger : Node
pauseManager.OnPauseStateChanged += OnPauseStateChanged; pauseManager.OnPauseStateChanged += OnPauseStateChanged;
} }
private void OnPauseStateChanged(PauseGroup group, bool isPaused) private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
{ {
var pauseManager = this.GetUtility<IPauseStackManager>(); var pauseManager = this.GetUtility<IPauseStackManager>();
GD.Print($"=== 暂停状态变化 ==="); GD.Print($"=== 暂停状态变化 ===");
GD.Print($"组: {group}"); GD.Print($"组: {e.Group}");
GD.Print($"状态: {(isPaused ? "暂停" : "恢复")}"); GD.Print($"状态: {(e.IsPaused ? "暂停" : "恢复")}");
GD.Print($"深度: {pauseManager.GetPauseDepth(group)}"); GD.Print($"深度: {pauseManager.GetPauseDepth(e.Group)}");
var reasons = pauseManager.GetPauseReasons(group); var reasons = pauseManager.GetPauseReasons(e.Group);
if (reasons.Count > 0) if (reasons.Count > 0)
{ {
GD.Print($"原因:"); GD.Print($"原因:");
@ -609,9 +609,9 @@ public partial class PauseDebugger : Node
7. **使用事件监听暂停状态**:实现响应式 UI 7. **使用事件监听暂停状态**:实现响应式 UI
```csharp ```csharp
pauseManager.OnPauseStateChanged += (group, isPaused) => pauseManager.OnPauseStateChanged += (_, e) =>
{ {
UpdateUI(isPaused); UpdateUI(e.IsPaused);
}; };
``` ```

1
packaging/_._ Normal file
View File

@ -0,0 +1 @@