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
// limitations under the License.
using System.Runtime.InteropServices;
namespace GFramework.Core.Abstractions.Concurrency;
/// <summary>
/// 锁统计信息
/// </summary>
[StructLayout(LayoutKind.Auto)]
public readonly struct LockStatistics
{
/// <summary>
@ -37,33 +40,4 @@ public readonly struct LockStatistics
/// 累计清理的锁数量
/// </summary>
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);
/// <summary>
/// 暂停状态变化事件
/// 暂停状态变化事件。
/// 事件遵循标准 .NET 事件模式,事件源为触发通知的暂停管理器实例,
/// 事件数据由 <see cref="PauseStateChangedEventArgs"/> 提供。
/// </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>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<WarningLevel>0</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Mediator.Abstractions" Version="3.0.1"/>

View File

@ -1,6 +1,5 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging.Appenders;
using NUnit.Framework;
namespace GFramework.Core.Tests.Logging;
@ -152,8 +151,12 @@ public class AsyncLogAppenderTests
[Test]
public void Append_WhenInnerAppenderThrows_ShouldNotCrash()
{
var reportedExceptions = new List<Exception>();
var innerAppender = new ThrowingAppender();
using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
using var asyncAppender = new AsyncLogAppender(
innerAppender,
bufferSize: 1000,
processingErrorHandler: reportedExceptions.Add);
// 即使内部 Appender 抛出异常,也不应该影响调用线程
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.Pause;
using NUnit.Framework;
namespace GFramework.Core.Tests.Pause;
@ -220,11 +219,11 @@ public class PauseStackManagerTests
PauseGroup? eventGroup = null;
bool? eventIsPaused = null;
_manager.OnPauseStateChanged += (group, isPaused) =>
_manager.OnPauseStateChanged += (_, e) =>
{
eventTriggered = true;
eventGroup = group;
eventIsPaused = isPaused;
eventGroup = e.Group;
eventIsPaused = e.IsPaused;
};
_manager.Push("Test", PauseGroup.Gameplay);
@ -243,10 +242,10 @@ public class PauseStackManagerTests
var token = _manager.Push("Test");
bool eventTriggered = false;
_manager.OnPauseStateChanged += (group, isPaused) =>
_manager.OnPauseStateChanged += (_, e) =>
{
eventTriggered = true;
Assert.That(isPaused, Is.False);
Assert.That(e.IsPaused, Is.False);
};
_manager.Pop(token);

View File

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

View File

@ -208,13 +208,10 @@ public class PriorityEvent<T> : IEvent
MergeAndSortHandlers(T t)
{
var (normalSnapshot, contextSnapshot) = CreateSnapshots();
// 使用快照避免迭代期间修改
// 使用统一的投影方法显式固定元组的可空标注,避免 LINQ 在 Concat 时推断出不兼容的签名。
return normalSnapshot
.Select(h => (h.Priority, Handler: (Action?)(() => h.Handler.Invoke(t)),
ContextHandler: (Action<EventContext<T>>?)null, IsContext: false))
.Concat(contextSnapshot
.Select(h => (h.Priority, Handler: (Action?)null,
ContextHandler: (Action<EventContext<T>>?)h.Handler, IsContext: true)))
.Select(h => CreateNormalHandlerInvocation(h, t))
.Concat(contextSnapshot.Select(CreateContextHandlerInvocation))
.OrderByDescending(h => h.Priority)
.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>

View File

@ -15,4 +15,5 @@ global using System;
global using System.Collections.Generic;
global using System.Linq;
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;
namespace GFramework.Core.Logging.Appenders;
/// <summary>
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
/// 异步日志输出器,使用 <see cref="Channel" /> 将调用线程与慢速日志目标解耦。
/// </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 CancellationTokenSource _cts;
private readonly SemaphoreSlim _flushSemaphore = new(0, 1);
private readonly ILogAppender _innerAppender;
private readonly Action<Exception>? _processingErrorHandler;
private readonly Task _processingTask;
private bool _disposed;
private volatile bool _flushRequested;
@ -21,9 +30,17 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
/// </summary>
/// <param name="innerAppender">内部日志输出器</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));
_processingErrorHandler = processingErrorHandler;
if (bufferSize <= 0)
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
@ -138,7 +155,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
}
/// <summary>
/// 后台处理日志的异步方法
/// 后台处理日志的异步方法。
/// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。
/// </summary>
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
{
@ -152,8 +170,8 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
}
catch (Exception ex)
{
// 记录内部错误到控制台(避免递归)
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}");
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。
ReportProcessingError(ex);
}
// 检查是否有刷新请求且通道已空
@ -175,7 +193,7 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
ReportProcessingError(ex);
}
finally
{
@ -184,10 +202,37 @@ public sealed class AsyncLogAppender : ILogAppender, IDisposable
{
_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
{
OnPauseStateChanged?.Invoke(group, false);
RaisePauseStateChanged(group, false);
}
catch (Exception ex)
{
@ -97,7 +97,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
/// <summary>
/// 暂停状态变化事件,当暂停状态发生改变时触发。
/// </summary>
public event Action<PauseGroup, bool>? OnPauseStateChanged;
public event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
/// <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>

View File

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

View File

@ -31,8 +31,9 @@ public interface ISceneRouteGuard : IRouteGuard<ISceneBehavior>
/// <summary>
/// 异步检查是否允许离开指定场景。
/// 该成员显式细化了通用路由守卫的离开检查,使场景守卫在 API 文档中保持场景语义。
/// </summary>
/// <param name="sceneKey">当前场景的唯一标识符。</param>
/// <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>
object View { get; }
/// <summary>
/// 获取键值
/// </summary>
/// <value>返回当前对象的键标识符</value>
string Key { get; }
/// <summary>
/// 获取页面是否处于活动状态
/// </summary>

View File

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

View File

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

View File

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

View File

@ -6,6 +6,9 @@
<Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<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>
<ItemGroup>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -100,8 +100,8 @@ void ClearAll();
void RegisterHandler(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;
}
private void OnPauseStateChanged(PauseGroup group, bool isPaused)
{
Console.WriteLine($"暂停状态变化: 组={group}, 暂停={isPaused}");
if (group == PauseGroup.Global)
{
if (isPaused)
{
ShowPauseIndicator();
}
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e)
{
Console.WriteLine($"暂停状态变化: 组={e.Group}, 暂停={e.IsPaused}");
if (e.Group == PauseGroup.Global)
{
if (e.IsPaused)
{
ShowPauseIndicator();
}
else
{
HidePauseIndicator();
@ -705,7 +705,7 @@ public partial class ProperCleanup : IController
_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: 监听暂停事件
var pauseManager = this.GetUtility<IPauseStackManager>();
pauseManager.OnPauseStateChanged += (group, isPaused) =>
{
if (group == PauseGroup.Global)
{
Visible = isPaused;
}
};
pauseManager.OnPauseStateChanged += (_, e) =>
{
if (e.Group == PauseGroup.Global)
{
Visible = e.IsPaused;
}
};
}
}
```
@ -887,15 +887,15 @@ public class PauseEventBridge : AbstractSystem
{
var pauseManager = this.GetUtility<IPauseStackManager>();
pauseManager.OnPauseStateChanged += (group, isPaused) =>
{
// 发送暂停事件
this.SendEvent(new GamePausedEvent
{
Group = group,
IsPaused = isPaused
});
};
pauseManager.OnPauseStateChanged += (_, e) =>
{
// 发送暂停事件
this.SendEvent(new GamePausedEvent
{
Group = e.Group,
IsPaused = e.IsPaused
});
};
}
}

View File

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

1
packaging/_._ Normal file
View File

@ -0,0 +1 @@