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

View File

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

View File

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

View File

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

View File

@ -15,5 +15,4 @@ 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.Channels;
global using System.Threading.Tasks;

View File

@ -1,26 +1,17 @@
using System.Threading.Channels;
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Logging.Appenders;
/// <summary>
/// 异步日志输出器,使用 <see cref="Channel" /> 将调用线程与慢速日志目标解耦。
/// 异步日志输出器,使用 Channel 实现非阻塞日志写入
/// </summary>
/// <remarks>
/// <para>
/// 该输出器在后台线程中顺序消费日志条目,因此调用方不会因为文件 IO 或其他慢速输出目标而阻塞。
/// </para>
/// <para>
/// 内部输出器抛出的异常不会重新抛回调用线程;如需观察后台处理失败,请在构造函数中提供
/// <c>processingErrorHandler</c> 回调。
/// </para>
/// </remarks>
public sealed class AsyncLogAppender : ILogAppender
public sealed class AsyncLogAppender : ILogAppender, IDisposable
{
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;
@ -30,17 +21,9 @@ public sealed class AsyncLogAppender : ILogAppender
/// </summary>
/// <param name="innerAppender">内部日志输出器</param>
/// <param name="bufferSize">缓冲区大小(默认 10000</param>
/// <param name="processingErrorHandler">
/// 后台处理日志时的错误回调。
/// 默认值为 <see langword="null" />,表示吞掉内部异常以避免污染宿主标准错误输出。
/// </param>
public AsyncLogAppender(
ILogAppender innerAppender,
int bufferSize = 10000,
Action<Exception>? processingErrorHandler = null)
public AsyncLogAppender(ILogAppender innerAppender, int bufferSize = 10000)
{
_innerAppender = innerAppender ?? throw new ArgumentNullException(nameof(innerAppender));
_processingErrorHandler = processingErrorHandler;
if (bufferSize <= 0)
throw new ArgumentException("Buffer size must be greater than 0.", nameof(bufferSize));
@ -155,8 +138,7 @@ public sealed class AsyncLogAppender : ILogAppender
}
/// <summary>
/// 后台处理日志的异步方法。
/// 该循环必须始终保持存活,因此所有内部异常都通过回调上报并被吞掉。
/// 后台处理日志的异步方法
/// </summary>
private async Task ProcessLogsAsync(CancellationToken cancellationToken)
{
@ -170,8 +152,8 @@ public sealed class AsyncLogAppender : ILogAppender
}
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)
{
ReportProcessingError(ex);
await Console.Error.WriteLineAsync($"[AsyncLogAppender] Fatal error in processing task: {ex}");
}
finally
{
@ -202,37 +184,10 @@ public sealed class AsyncLogAppender : ILogAppender
{
_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
{
RaisePauseStateChanged(group, false);
OnPauseStateChanged?.Invoke(group, false);
}
catch (Exception ex)
{
@ -97,7 +97,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
/// <summary>
/// 暂停状态变化事件,当暂停状态发生改变时触发。
/// </summary>
public event EventHandler<PauseStateChangedEventArgs>? OnPauseStateChanged;
public event Action<PauseGroup, bool>? OnPauseStateChanged;
/// <summary>
/// 推入一个新的暂停请求到指定的暂停组中。
@ -488,18 +488,7 @@ public class PauseStackManager : AbstractContextUtility, IPauseStackManager, IAs
}
// 触发事件
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));
OnPauseStateChanged?.Invoke(group, isPaused);
}
/// <summary>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1 +0,0 @@