Compare commits

..

No commits in common. "63b1d71a0e2b4622df2ea0f9d704e86853a62263" and "51492b1dcd73c0ff65676f0aaca25f7f7890687f" have entirely different histories.

30 changed files with 140 additions and 322 deletions

View File

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

View File

@ -1,30 +0,0 @@
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,7 +7,6 @@
<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,5 +1,6 @@
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;
@ -151,12 +152,8 @@ 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( using var asyncAppender = new AsyncLogAppender(innerAppender, bufferSize: 1000);
innerAppender,
bufferSize: 1000,
processingErrorHandler: reportedExceptions.Add);
// 即使内部 Appender 抛出异常,也不应该影响调用线程 // 即使内部 Appender 抛出异常,也不应该影响调用线程
Assert.DoesNotThrow(() => Assert.DoesNotThrow(() =>
@ -168,56 +165,7 @@ public class AsyncLogAppenderTests
} }
}); });
asyncAppender.Flush(); Thread.Sleep(100); // 等待后台处理
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);
} }
// 辅助测试类 // 辅助测试类
@ -280,20 +228,4 @@ 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,5 +1,6 @@
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;
@ -219,11 +220,11 @@ public class PauseStackManagerTests
PauseGroup? eventGroup = null; PauseGroup? eventGroup = null;
bool? eventIsPaused = null; bool? eventIsPaused = null;
_manager.OnPauseStateChanged += (_, e) => _manager.OnPauseStateChanged += (group, isPaused) =>
{ {
eventTriggered = true; eventTriggered = true;
eventGroup = e.Group; eventGroup = group;
eventIsPaused = e.IsPaused; eventIsPaused = isPaused;
}; };
_manager.Push("Test", PauseGroup.Gameplay); _manager.Push("Test", PauseGroup.Gameplay);
@ -242,10 +243,10 @@ public class PauseStackManagerTests
var token = _manager.Push("Test"); var token = _manager.Push("Test");
bool eventTriggered = false; bool eventTriggered = false;
_manager.OnPauseStateChanged += (_, e) => _manager.OnPauseStateChanged += (group, isPaused) =>
{ {
eventTriggered = true; eventTriggered = true;
Assert.That(e.IsPaused, Is.False); Assert.That(isPaused, Is.False);
}; };
_manager.Pop(token); _manager.Pop(token);

View File

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

View File

@ -208,10 +208,13 @@ 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 => CreateNormalHandlerInvocation(h, t)) .Select(h => (h.Priority, Handler: (Action?)(() => h.Handler.Invoke(t)),
.Concat(contextSnapshot.Select(CreateContextHandlerInvocation)) ContextHandler: (Action<EventContext<T>>?)null, IsContext: false))
.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();
} }
@ -286,29 +289,6 @@ 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,5 +15,4 @@ 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,26 +1,17 @@
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>
/// 异步日志输出器,使用 <see cref="Channel" /> 将调用线程与慢速日志目标解耦。 /// 异步日志输出器,使用 Channel 实现非阻塞日志写入
/// </summary> /// </summary>
/// <remarks> public sealed class AsyncLogAppender : ILogAppender, IDisposable
/// <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;
@ -30,17 +21,9 @@ public sealed class AsyncLogAppender : ILogAppender
/// </summary> /// </summary>
/// <param name="innerAppender">内部日志输出器</param> /// <param name="innerAppender">内部日志输出器</param>
/// <param name="bufferSize">缓冲区大小(默认 10000</param> /// <param name="bufferSize">缓冲区大小(默认 10000</param>
/// <param name="processingErrorHandler"> public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000)
/// 后台处理日志时的错误回调。
/// 默认值为 <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));
@ -155,8 +138,7 @@ public sealed class AsyncLogAppender : ILogAppender
} }
/// <summary> /// <summary>
/// 后台处理日志的异步方法。 /// 后台处理日志的异步方法
/// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。
/// </summary> /// </summary>
private async Task ProcessLogsAsync(CancellationToken cancellationToken) private async Task ProcessLogsAsync(CancellationToken cancellationToken)
{ {
@ -170,8 +152,8 @@ public sealed class AsyncLogAppender : ILogAppender
} }
catch (Exception ex) catch (Exception ex)
{ {
// 后台消费失败只通过显式回调暴露,避免测试宿主将 stderr 误判为测试告警。 // 记录内部错误到控制台(避免递归)
ReportProcessingError(ex); await Console.Error.WriteLineAsync($"[AsyncLogAppender] Error processing log entry: {ex.Message}");
} }
// 检查是否有刷新请求且通道已空 // 检查是否有刷新请求且通道已空
@ -193,7 +175,7 @@ public sealed class AsyncLogAppender : ILogAppender
} }
catch (Exception ex) catch (Exception ex)
{ {
ReportProcessingError(ex); await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
} }
finally finally
{ {
@ -202,37 +184,10 @@ public sealed class AsyncLogAppender : ILogAppender
{ {
_innerAppender.Flush(); _innerAppender.Flush();
} }
catch (Exception ex) catch
{ {
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
{ {
RaisePauseStateChanged(group, false); OnPauseStateChanged?.Invoke(group, false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -97,7 +97,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
/// <summary> /// <summary>
/// 暂停状态变化事件,当暂停状态发生改变时触发。 /// 暂停状态变化事件,当暂停状态发生改变时触发。
/// </summary> /// </summary>
public event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged; public event Action<PauseGroup, bool>? OnPauseStateChanged;
/// <summary> /// <summary>
/// 推入一个新的暂停请求到指定的暂停组中。 /// 推入一个新的暂停请求到指定的暂停组中。
@ -488,18 +488,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
} }
// 触发事件 // 触发事件
RaisePauseStateChanged(group, isPaused); OnPauseStateChanged?.Invoke(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,6 +21,12 @@ 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,9 +31,8 @@ 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>
new ValueTask<bool> CanLeaveAsync(string sceneKey); ValueTask<bool> CanLeaveAsync(string sceneKey);
} }

View File

@ -46,6 +46,14 @@ 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,10 +17,9 @@ 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>
new ValueTask<bool> CanLeaveAsync(string uiKey); 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.0</TargetFramework> <TargetFramework>netstandard2.1</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.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<!-- 这是 Analyzer不是运行时库 --> <!-- 这是 Analyzer不是运行时库 -->
<IsRoslynAnalyzer>true</IsRoslynAnalyzer> <IsRoslynAnalyzer>true</IsRoslynAnalyzer>

View File

@ -6,9 +6,6 @@
<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>
/// 标记类的优先级,自动生成 <c>GFramework.Core.Abstractions.Bases.IPrioritized</c> 接口实现。 /// 标记类的优先级,自动生成 <see cref="GFramework.Core.Abstractions.Bases.IPrioritized"/> 接口实现
/// </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.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild> <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources> <EmbedUntrackedSources>true</EmbedUntrackedSources>
<!-- <!--

View File

@ -14,6 +14,9 @@
<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.0</TargetFramework> <TargetFramework>netstandard2.1</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.IsKind(SyntaxKind.PartialKeyword))) if (syntax.Modifiers.All(m => m.Kind() != 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.0</TargetFramework> <TargetFramework>netstandard2.1</TargetFramework>
<!-- 这是 Analyzer不是运行时库 --> <!-- 这是 Analyzer不是运行时库 -->
<IsRoslynAnalyzer>true</IsRoslynAnalyzer> <IsRoslynAnalyzer>true</IsRoslynAnalyzer>
@ -30,6 +30,7 @@
<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,16 +16,11 @@
<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 EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged; event Action<PauseGroup, bool>? OnPauseStateChanged;
``` ```
## 基本用法 ## 基本用法
@ -377,16 +377,16 @@ public partial class PauseIndicator : IController
_pauseManager.OnPauseStateChanged += OnPauseStateChanged; _pauseManager.OnPauseStateChanged += OnPauseStateChanged;
} }
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e) private void OnPauseStateChanged(PauseGroup group, bool isPaused)
{ {
Console.WriteLine($"暂停状态变化: 组={e.Group}, 暂停={e.IsPaused}"); Console.WriteLine($"暂停状态变化: 组={group}, 暂停={isPaused}");
if (e.Group == PauseGroup.Global) if (group == PauseGroup.Global)
{ {
if (e.IsPaused) if (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(object? sender, PauseStateChangedEventArgs e) { } private void OnPauseChanged(PauseGroup group, bool isPaused) { }
} }
``` ```
@ -743,13 +743,13 @@ public partial class PauseMenu : Control
// 方案 2: 监听暂停事件 // 方案 2: 监听暂停事件
var pauseManager = this.GetUtility<IPauseStackManager>(); var pauseManager = this.GetUtility<IPauseStackManager>();
pauseManager.OnPauseStateChanged += (_, e) => pauseManager.OnPauseStateChanged += (group, isPaused) =>
{ {
if (e.Group == PauseGroup.Global) if (group == PauseGroup.Global)
{ {
Visible = e.IsPaused; Visible = isPaused;
} }
}; };
} }
} }
``` ```
@ -887,15 +887,15 @@ public class PauseEventBridge : AbstractSystem
{ {
var pauseManager = this.GetUtility<IPauseStackManager>(); var pauseManager = this.GetUtility<IPauseStackManager>();
pauseManager.OnPauseStateChanged += (_, e) => pauseManager.OnPauseStateChanged += (group, isPaused) =>
{ {
// 发送暂停事件 // 发送暂停事件
this.SendEvent(new GamePausedEvent this.SendEvent(new GamePausedEvent
{ {
Group = e.Group, Group = group,
IsPaused = e.IsPaused IsPaused = isPaused
}); });
}; };
} }
} }

View File

@ -44,7 +44,7 @@ public interface IPauseStackManager : IContextUtility
int GetPauseDepth(PauseGroup group = PauseGroup.Global); int GetPauseDepth(PauseGroup group = PauseGroup.Global);
// 暂停状态变化事件 // 暂停状态变化事件
event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged; event Action<PauseGroup, bool>? OnPauseStateChanged;
} }
``` ```
@ -477,12 +477,12 @@ public partial class PauseIndicator : Label
pauseManager.OnPauseStateChanged -= OnPauseStateChanged; pauseManager.OnPauseStateChanged -= OnPauseStateChanged;
} }
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e) private void OnPauseStateChanged(PauseGroup group, bool isPaused)
{ {
if (e.Group == PauseGroup.Global) if (group == PauseGroup.Global)
{ {
Text = e.IsPaused ? "游戏已暂停" : "游戏运行中"; Text = isPaused ? "游戏已暂停" : "游戏运行中";
Visible = e.IsPaused; Visible = isPaused;
} }
} }
} }
@ -502,16 +502,16 @@ public partial class PauseDebugger : Node
pauseManager.OnPauseStateChanged += OnPauseStateChanged; pauseManager.OnPauseStateChanged += OnPauseStateChanged;
} }
private void OnPauseStateChanged(object? sender, PauseStateChangedEventArgs e) private void OnPauseStateChanged(PauseGroup group, bool isPaused)
{ {
var pauseManager = this.GetUtility<IPauseStackManager>(); var pauseManager = this.GetUtility<IPauseStackManager>();
GD.Print($"=== 暂停状态变化 ==="); GD.Print($"=== 暂停状态变化 ===");
GD.Print($"组: {e.Group}"); GD.Print($"组: {group}");
GD.Print($"状态: {(e.IsPaused ? "暂停" : "恢复")}"); GD.Print($"状态: {(isPaused ? "暂停" : "恢复")}");
GD.Print($"深度: {pauseManager.GetPauseDepth(e.Group)}"); GD.Print($"深度: {pauseManager.GetPauseDepth(group)}");
var reasons = pauseManager.GetPauseReasons(e.Group); var reasons = pauseManager.GetPauseReasons(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 += (_, e) => pauseManager.OnPauseStateChanged += (group, isPaused) =>
{ {
UpdateUI(e.IsPaused); UpdateUI(isPaused);
}; };
``` ```

View File

@ -1 +0,0 @@