mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 02:24:30 +08:00
refactor(game): 重构路由系统并优化CI测试流程
- 将SceneRouterBase和UiRouterBase继承自新的RouterBase基类 - 移除原有的守卫管理相关代码,统一使用基类实现 - 更新路由栈操作使用基类提供的Stack属性 - 重写Current、Contains等方法以使用基类实现 - 在CI工作流中启用并发测试执行以提升性能 - 添加等待步骤确保并发测试完成 - 更新项目文件排除测试项目的编译 - 在解决方案文件中添加GFramework.Game.Tests项目引用 - 新增RouterBase基类提供通用路由管理功能
This commit is contained in:
parent
27858df94e
commit
4afa856fdc
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@ -113,29 +113,42 @@ jobs:
|
|||||||
run: dotnet build -c Release --no-restore
|
run: dotnet build -c Release --no-restore
|
||||||
|
|
||||||
# 运行单元测试,输出TRX格式结果到TestResults目录
|
# 运行单元测试,输出TRX格式结果到TestResults目录
|
||||||
|
# 使用并发执行以加快测试速度
|
||||||
- name: Test - Core
|
- name: Test - Core
|
||||||
run: |
|
run: |
|
||||||
dotnet test GFramework.Core.Tests \
|
dotnet test GFramework.Core.Tests \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=core-$RANDOM.trx" \
|
--logger "trx;LogFileName=core-$RANDOM.trx" \
|
||||||
--results-directory TestResults
|
--results-directory TestResults &
|
||||||
|
|
||||||
|
- name: Test - Game
|
||||||
|
run: |
|
||||||
|
dotnet test GFramework.Game.Tests \
|
||||||
|
-c Release \
|
||||||
|
--no-build \
|
||||||
|
--logger "trx;LogFileName=game-$RANDOM.trx" \
|
||||||
|
--results-directory TestResults &
|
||||||
|
|
||||||
- name: Test - SourceGenerators
|
- name: Test - SourceGenerators
|
||||||
run: |
|
run: |
|
||||||
dotnet test GFramework.SourceGenerators.Tests \
|
dotnet test GFramework.SourceGenerators.Tests \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=sg-$RANDOM.trx" \
|
--logger "trx;LogFileName=sg-$RANDOM.trx" \
|
||||||
--results-directory TestResults
|
--results-directory TestResults &
|
||||||
|
|
||||||
- name: Test - GFramework.Ecs.Arch.Tests
|
- name: Test - ECS Arch
|
||||||
run: |
|
run: |
|
||||||
dotnet test GFramework.Ecs.Arch.Tests \
|
dotnet test GFramework.Ecs.Arch.Tests \
|
||||||
-c Release \
|
-c Release \
|
||||||
--no-build \
|
--no-build \
|
||||||
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
|
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
|
||||||
--results-directory TestResults
|
--results-directory TestResults &
|
||||||
|
|
||||||
|
# 等待所有并发测试完成
|
||||||
|
- name: Wait for tests
|
||||||
|
run: wait
|
||||||
|
|
||||||
- name: Generate CTRF report
|
- name: Generate CTRF report
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
25
GFramework.Game.Abstractions/Routing/IRoute.cs
Normal file
25
GFramework.Game.Abstractions/Routing/IRoute.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) 2026 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.Game.Abstractions.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由项接口,表示可路由的对象
|
||||||
|
/// </summary>
|
||||||
|
public interface IRoute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 路由键值,用于唯一标识路由项
|
||||||
|
/// </summary>
|
||||||
|
string Key { get; }
|
||||||
|
}
|
||||||
26
GFramework.Game.Abstractions/Routing/IRouteContext.cs
Normal file
26
GFramework.Game.Abstractions/Routing/IRouteContext.cs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright (c) 2026 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.Game.Abstractions.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由上下文接口,表示路由进入时的参数
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 这是一个标记接口,用于类型约束。
|
||||||
|
/// 具体的路由上下文类型应该实现此接口。
|
||||||
|
/// </remarks>
|
||||||
|
public interface IRouteContext
|
||||||
|
{
|
||||||
|
// 标记接口,用于类型约束
|
||||||
|
}
|
||||||
54
GFramework.Game.Abstractions/Routing/IRouteGuard.cs
Normal file
54
GFramework.Game.Abstractions/Routing/IRouteGuard.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) 2026 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.Game.Abstractions.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由守卫接口,用于控制路由的进入和离开
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRoute">路由项类型</typeparam>
|
||||||
|
public interface IRouteGuard<TRoute> where TRoute : IRoute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 守卫优先级,数值越小优先级越高
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 守卫按优先级从小到大依次执行。
|
||||||
|
/// 建议使用 0-100 的范围,默认为 50。
|
||||||
|
/// </remarks>
|
||||||
|
int Priority { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否可以中断后续守卫的执行
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 如果为 true,当此守卫返回 true 或抛出异常时,将中断后续守卫的执行。
|
||||||
|
/// 如果为 false,将继续执行后续守卫。
|
||||||
|
/// </remarks>
|
||||||
|
bool CanInterrupt { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查是否可以进入指定路由
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeKey">路由键值</param>
|
||||||
|
/// <param name="context">路由上下文</param>
|
||||||
|
/// <returns>如果允许进入返回 true,否则返回 false</returns>
|
||||||
|
ValueTask<bool> CanEnterAsync(string routeKey, IRouteContext? context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查是否可以离开指定路由
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeKey">路由键值</param>
|
||||||
|
/// <returns>如果允许离开返回 true,否则返回 false</returns>
|
||||||
|
ValueTask<bool> CanLeaveAsync(string routeKey);
|
||||||
|
}
|
||||||
@ -2,22 +2,24 @@
|
|||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// 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 GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Scene;
|
namespace GFramework.Game.Abstractions.Scene;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 场景行为接口,定义了场景生命周期管理的标准方法。
|
/// 场景行为接口,定义了场景生命周期管理的标准方法。
|
||||||
/// 实现此接口的类需要处理场景的加载、激活、暂停、恢复和卸载等核心操作。
|
/// 实现此接口的类需要处理场景的加载、激活、暂停、恢复和卸载等核心操作。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISceneBehavior
|
public interface ISceneBehavior : IRoute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取场景的唯一标识符。
|
/// 获取场景的唯一标识符。
|
||||||
|
|||||||
@ -2,19 +2,21 @@
|
|||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// 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 GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Scene;
|
namespace GFramework.Game.Abstractions.Scene;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 场景进入参数接口
|
/// 场景进入参数接口
|
||||||
/// 该接口用于定义场景跳转时传递的参数数据结构
|
/// 该接口用于定义场景跳转时传递的参数数据结构
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISceneEnterParam;
|
public interface ISceneEnterParam : IRouteContext;
|
||||||
@ -11,28 +11,16 @@
|
|||||||
// 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 GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.Scene;
|
namespace GFramework.Game.Abstractions.Scene;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 场景路由守卫接口,用于在场景切换前进行权限检查和条件验证。
|
/// 场景路由守卫接口,用于在场景切换前进行权限检查和条件验证。
|
||||||
/// 实现此接口可以拦截场景的进入和离开操作。
|
/// 实现此接口可以拦截场景的进入和离开操作。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ISceneRouteGuard
|
public interface ISceneRouteGuard : IRouteGuard<ISceneBehavior>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 获取守卫的执行优先级。
|
|
||||||
/// 数值越小优先级越高,越先执行。
|
|
||||||
/// 建议范围:-1000 到 1000。
|
|
||||||
/// </summary>
|
|
||||||
int Priority { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取守卫是否可以中断后续守卫的执行。
|
|
||||||
/// true 表示当前守卫通过后,可以跳过后续守卫直接允许操作。
|
|
||||||
/// false 表示即使当前守卫通过,仍需执行所有后续守卫。
|
|
||||||
/// </summary>
|
|
||||||
bool CanInterrupt { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步检查是否允许进入指定场景。
|
/// 异步检查是否允许进入指定场景。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -46,5 +34,5 @@ public interface ISceneRouteGuard
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sceneKey">当前场景的唯一标识符。</param>
|
/// <param name="sceneKey">当前场景的唯一标识符。</param>
|
||||||
/// <returns>如果允许离开则返回 true,否则返回 false。</returns>
|
/// <returns>如果允许离开则返回 true,否则返回 false。</returns>
|
||||||
Task<bool> CanLeaveAsync(string sceneKey);
|
new Task<bool> CanLeaveAsync(string sceneKey);
|
||||||
}
|
}
|
||||||
@ -1,11 +1,12 @@
|
|||||||
using GFramework.Game.Abstractions.Enums;
|
using GFramework.Game.Abstractions.Enums;
|
||||||
|
using GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI;
|
namespace GFramework.Game.Abstractions.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI页面行为接口,定义了UI页面的生命周期方法和状态管理
|
/// UI页面行为接口,定义了UI页面的生命周期方法和状态管理
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUiPageBehavior
|
public interface IUiPageBehavior : IRoute
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置当前UI句柄。
|
/// 获取或设置当前UI句柄。
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
namespace GFramework.Game.Abstractions.UI;
|
using GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Abstractions.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI页面进入参数接口
|
/// UI页面进入参数接口
|
||||||
/// 该接口用于定义UI页面跳转时传递的参数数据结构
|
/// 该接口用于定义UI页面跳转时传递的参数数据结构
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUiPageEnterParam;
|
public interface IUiPageEnterParam : IRouteContext;
|
||||||
@ -1,22 +1,13 @@
|
|||||||
|
using GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.Abstractions.UI;
|
namespace GFramework.Game.Abstractions.UI;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI路由守卫接口
|
/// UI路由守卫接口
|
||||||
/// 用于拦截和处理UI路由切换,实现业务逻辑解耦
|
/// 用于拦截和处理UI路由切换,实现业务逻辑解耦
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IUiRouteGuard
|
public interface IUiRouteGuard : IRouteGuard<IUiPageBehavior>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// 守卫优先级,数值越小越先执行
|
|
||||||
/// </summary>
|
|
||||||
int Priority { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 是否可中断后续守卫
|
|
||||||
/// 如果返回 true,当该守卫返回 false 时,将停止执行后续守卫
|
|
||||||
/// </summary>
|
|
||||||
bool CanInterrupt { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 进入UI前的检查
|
/// 进入UI前的检查
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -30,5 +21,5 @@ public interface IUiRouteGuard
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uiKey">当前UI标识符</param>
|
/// <param name="uiKey">当前UI标识符</param>
|
||||||
/// <returns>true表示允许离开,false表示拦截</returns>
|
/// <returns>true表示允许离开,false表示拦截</returns>
|
||||||
Task<bool> CanLeaveAsync(string uiKey);
|
new Task<bool> CanLeaveAsync(string uiKey);
|
||||||
}
|
}
|
||||||
@ -113,28 +113,6 @@ public interface IUiRouter : ISystem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool Contains(string uiKey);
|
bool Contains(string uiKey);
|
||||||
|
|
||||||
#region 路由守卫
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册路由守卫
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard">守卫实例</param>
|
|
||||||
void AddGuard(IUiRouteGuard guard);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册路由守卫(泛型方法)
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">守卫类型,必须实现 IUiRouteGuard 且有无参构造函数</typeparam>
|
|
||||||
void AddGuard<T>() where T : IUiRouteGuard, new();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 移除路由守卫
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard">守卫实例</param>
|
|
||||||
void RemoveGuard(IUiRouteGuard guard);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Layer UI
|
#region Layer UI
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
23
GFramework.Game.Tests/GFramework.Game.Tests.csproj
Normal file
23
GFramework.Game.Tests/GFramework.Game.Tests.csproj
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0"/>
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72"/>
|
||||||
|
<PackageReference Include="NUnit" Version="4.5.1"/>
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="6.1.0"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\GFramework.Game\GFramework.Game.csproj"/>
|
||||||
|
<ProjectReference Include="..\GFramework.Core\GFramework.Core.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
19
GFramework.Game.Tests/GlobalUsings.cs
Normal file
19
GFramework.Game.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) 2026 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.
|
||||||
|
|
||||||
|
global using NUnit.Framework;
|
||||||
|
global using Moq;
|
||||||
|
global using System;
|
||||||
|
global using System.Collections.Generic;
|
||||||
|
global using System.Linq;
|
||||||
|
global using System.Threading.Tasks;
|
||||||
582
GFramework.Game.Tests/Routing/RouterBaseTests.cs
Normal file
582
GFramework.Game.Tests/Routing/RouterBaseTests.cs
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
// Copyright (c) 2026 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.
|
||||||
|
|
||||||
|
using GFramework.Game.Abstractions.Routing;
|
||||||
|
using GFramework.Game.Routing;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Tests.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RouterBase 单元测试
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public class RouterBaseTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用路由项
|
||||||
|
/// </summary>
|
||||||
|
private class TestRoute : IRoute
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用路由上下文
|
||||||
|
/// </summary>
|
||||||
|
private class TestContext : IRouteContext
|
||||||
|
{
|
||||||
|
public string? Data { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用路由守卫
|
||||||
|
/// </summary>
|
||||||
|
private class TestGuard : IRouteGuard<TestRoute>
|
||||||
|
{
|
||||||
|
public Func<string, IRouteContext?, ValueTask<bool>>? EnterFunc { get; set; }
|
||||||
|
public Func<string, ValueTask<bool>>? LeaveFunc { get; set; }
|
||||||
|
public int Priority { get; set; }
|
||||||
|
public bool CanInterrupt { get; set; }
|
||||||
|
|
||||||
|
public ValueTask<bool> CanEnterAsync(string routeKey, IRouteContext? context)
|
||||||
|
{
|
||||||
|
return EnterFunc?.Invoke(routeKey, context) ?? ValueTask.FromResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask<bool> CanLeaveAsync(string routeKey)
|
||||||
|
{
|
||||||
|
return LeaveFunc?.Invoke(routeKey) ?? ValueTask.FromResult(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试用路由器实现
|
||||||
|
/// </summary>
|
||||||
|
private class TestRouter : RouterBase<TestRoute, TestContext>
|
||||||
|
{
|
||||||
|
public bool HandlersRegistered { get; private set; }
|
||||||
|
|
||||||
|
// 暴露 Stack 用于测试
|
||||||
|
public new Stack<TestRoute> Stack => base.Stack;
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
// 测试用路由器不需要初始化逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void RegisterHandlers()
|
||||||
|
{
|
||||||
|
HandlersRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露 protected 方法用于测试
|
||||||
|
public new Task<bool> ExecuteEnterGuardsAsync(string routeKey, TestContext? context)
|
||||||
|
{
|
||||||
|
return base.ExecuteEnterGuardsAsync(routeKey, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public new Task<bool> ExecuteLeaveGuardsAsync(string routeKey)
|
||||||
|
{
|
||||||
|
return base.ExecuteLeaveGuardsAsync(routeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddGuard_ShouldAddGuardToList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard { Priority = 10 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Assert - 通过尝试添加相同守卫来验证
|
||||||
|
Assert.DoesNotThrow(() => router.AddGuard(guard));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddGuard_ShouldSortByPriority()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard1 = new TestGuard { Priority = 20 };
|
||||||
|
var guard2 = new TestGuard { Priority = 10 };
|
||||||
|
var guard3 = new TestGuard { Priority = 30 };
|
||||||
|
|
||||||
|
// Act
|
||||||
|
router.AddGuard(guard1);
|
||||||
|
router.AddGuard(guard2);
|
||||||
|
router.AddGuard(guard3);
|
||||||
|
|
||||||
|
// Assert - 通过执行守卫来验证顺序
|
||||||
|
var executionOrder = new List<int>();
|
||||||
|
guard1.EnterFunc = (_, _) =>
|
||||||
|
{
|
||||||
|
executionOrder.Add(1);
|
||||||
|
return ValueTask.FromResult(true);
|
||||||
|
};
|
||||||
|
guard2.EnterFunc = (_, _) =>
|
||||||
|
{
|
||||||
|
executionOrder.Add(2);
|
||||||
|
return ValueTask.FromResult(true);
|
||||||
|
};
|
||||||
|
guard3.EnterFunc = (_, _) =>
|
||||||
|
{
|
||||||
|
executionOrder.Add(3);
|
||||||
|
return ValueTask.FromResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
router.ExecuteEnterGuardsAsync("test", null).Wait();
|
||||||
|
|
||||||
|
Assert.That(executionOrder, Is.EqualTo(new[] { 2, 1, 3 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddGuard_WithGeneric_ShouldCreateAndAddGuard()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.DoesNotThrow(() => router.AddGuard<TestGuard>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void AddGuard_WithNull_ShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => router.AddGuard(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveGuard_ShouldRemoveGuardFromList()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard { Priority = 10 };
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
router.RemoveGuard(guard);
|
||||||
|
|
||||||
|
// Assert - 守卫应该被移除,不会再执行
|
||||||
|
var executed = false;
|
||||||
|
guard.EnterFunc = (_, _) =>
|
||||||
|
{
|
||||||
|
executed = true;
|
||||||
|
return ValueTask.FromResult(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
router.ExecuteEnterGuardsAsync("test", null).Wait();
|
||||||
|
|
||||||
|
Assert.That(executed, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void RemoveGuard_WithNull_ShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
Assert.Throws<ArgumentNullException>(() => router.RemoveGuard(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteEnterGuardsAsync_WithNoGuards_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteEnterGuardsAsync_WithAllowingGuard_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
EnterFunc = (_, _) => ValueTask.FromResult(true)
|
||||||
|
};
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteEnterGuardsAsync_WithBlockingGuard_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
EnterFunc = (_, _) => ValueTask.FromResult(false)
|
||||||
|
};
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteEnterGuardsAsync_WithInterruptingGuard_ShouldStopExecution()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard1 = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
CanInterrupt = true,
|
||||||
|
EnterFunc = (_, _) => ValueTask.FromResult(true)
|
||||||
|
};
|
||||||
|
var guard2Executed = false;
|
||||||
|
var guard2 = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 20,
|
||||||
|
EnterFunc = (_, _) =>
|
||||||
|
{
|
||||||
|
guard2Executed = true;
|
||||||
|
return ValueTask.FromResult(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router.AddGuard(guard1);
|
||||||
|
router.AddGuard(guard2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
Assert.That(guard2Executed, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteEnterGuardsAsync_WithThrowingGuard_ShouldContinueIfNotInterrupting()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard1 = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
CanInterrupt = false,
|
||||||
|
EnterFunc = (_, _) => throw new InvalidOperationException("Test exception")
|
||||||
|
};
|
||||||
|
var guard2Executed = false;
|
||||||
|
var guard2 = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 20,
|
||||||
|
EnterFunc = (_, _) =>
|
||||||
|
{
|
||||||
|
guard2Executed = true;
|
||||||
|
return ValueTask.FromResult(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
router.AddGuard(guard1);
|
||||||
|
router.AddGuard(guard2);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
Assert.That(guard2Executed, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteEnterGuardsAsync_WithThrowingInterruptingGuard_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
CanInterrupt = true,
|
||||||
|
EnterFunc = (_, _) => throw new InvalidOperationException("Test exception")
|
||||||
|
};
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteEnterGuardsAsync("test", null);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteLeaveGuardsAsync_WithNoGuards_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteLeaveGuardsAsync("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteLeaveGuardsAsync_WithAllowingGuard_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
LeaveFunc = _ => ValueTask.FromResult(true)
|
||||||
|
};
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteLeaveGuardsAsync("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task ExecuteLeaveGuardsAsync_WithBlockingGuard_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var guard = new TestGuard
|
||||||
|
{
|
||||||
|
Priority = 10,
|
||||||
|
LeaveFunc = _ => ValueTask.FromResult(false)
|
||||||
|
};
|
||||||
|
router.AddGuard(guard);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await router.ExecuteLeaveGuardsAsync("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Contains_WithEmptyStack_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Contains("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Contains_WithMatchingRoute_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Contains("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Contains_WithNonMatchingRoute_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test1" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Contains("test2");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PeekKey_WithEmptyStack_ShouldReturnEmptyString()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.PeekKey();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PeekKey_WithRoute_ShouldReturnRouteKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.PeekKey();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsTop_WithEmptyStack_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.IsTop("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsTop_WithMatchingRoute_ShouldReturnTrue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.IsTop("test");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void IsTop_WithNonMatchingRoute_ShouldReturnFalse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test1" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.IsTop("test2");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Current_WithEmptyStack_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Current;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Current_WithRoute_ShouldReturnTopRoute()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Current;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(route));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CurrentKey_WithEmptyStack_ShouldReturnNull()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.CurrentKey;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void CurrentKey_WithRoute_ShouldReturnRouteKey()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
var route = new TestRoute { Key = "test" };
|
||||||
|
router.Stack.Push(route);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.CurrentKey;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo("test"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Count_WithEmptyStack_ShouldReturnZero()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Count;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void Count_WithRoutes_ShouldReturnCorrectCount()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var router = new TestRouter();
|
||||||
|
router.Stack.Push(new TestRoute { Key = "test1" });
|
||||||
|
router.Stack.Push(new TestRoute { Key = "test2" });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = router.Count;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.That(result, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
244
GFramework.Game/Routing/RouterBase.cs
Normal file
244
GFramework.Game/Routing/RouterBase.cs
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
// Copyright (c) 2026 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.
|
||||||
|
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
using GFramework.Core.Logging;
|
||||||
|
using GFramework.Core.Systems;
|
||||||
|
using GFramework.Game.Abstractions.Routing;
|
||||||
|
|
||||||
|
namespace GFramework.Game.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由器基类,提供通用的路由管理功能
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRoute">路由项类型,必须实现 IRoute 接口</typeparam>
|
||||||
|
/// <typeparam name="TContext">路由上下文类型,必须实现 IRouteContext 接口</typeparam>
|
||||||
|
/// <remarks>
|
||||||
|
/// 此基类提供了以下通用功能:
|
||||||
|
/// - 路由守卫管理 (AddGuard/RemoveGuard)
|
||||||
|
/// - 守卫执行逻辑 (ExecuteEnterGuardsAsync/ExecuteLeaveGuardsAsync)
|
||||||
|
/// - 路由栈管理 (Stack/Current/CurrentKey)
|
||||||
|
/// - 栈操作方法 (Contains/PeekKey/IsTop)
|
||||||
|
/// </remarks>
|
||||||
|
public abstract class RouterBase<TRoute, TContext> : AbstractSystem
|
||||||
|
where TRoute : IRoute
|
||||||
|
where TContext : IRouteContext
|
||||||
|
{
|
||||||
|
private static readonly ILogger Log =
|
||||||
|
LoggerFactoryResolver.Provider.CreateLogger(nameof(RouterBase<TRoute, TContext>));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由守卫列表,按优先级排序
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<IRouteGuard<TRoute>> _guards = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由栈,用于管理路由的显示顺序和导航历史
|
||||||
|
/// </summary>
|
||||||
|
protected readonly Stack<TRoute> Stack = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前路由 (栈顶元素)
|
||||||
|
/// </summary>
|
||||||
|
public TRoute? Current => Stack.Count > 0 ? Stack.Peek() : default;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前路由的键值
|
||||||
|
/// </summary>
|
||||||
|
public string? CurrentKey => Current?.Key;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取栈深度
|
||||||
|
/// </summary>
|
||||||
|
public int Count => Stack.Count;
|
||||||
|
|
||||||
|
#region Abstract Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册过渡处理器 (由子类实现)
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 子类应该在此方法中注册所有需要的过渡处理器。
|
||||||
|
/// 此方法在 OnInit 中被调用。
|
||||||
|
/// </remarks>
|
||||||
|
protected abstract void RegisterHandlers();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Guard Management
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加路由守卫
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guard">路由守卫实例</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当守卫实例为 null 时抛出</exception>
|
||||||
|
public void AddGuard(IRouteGuard<TRoute> guard)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(guard);
|
||||||
|
|
||||||
|
if (_guards.Contains(guard))
|
||||||
|
{
|
||||||
|
Log.Debug("Guard already registered: {0}", guard.GetType().Name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_guards.Add(guard);
|
||||||
|
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
||||||
|
Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加路由守卫 (泛型版本)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">守卫类型,必须实现 IRouteGuard 接口且有无参构造函数</typeparam>
|
||||||
|
public void AddGuard<T>() where T : IRouteGuard<TRoute>, new()
|
||||||
|
{
|
||||||
|
AddGuard(new T());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 移除路由守卫
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="guard">要移除的路由守卫实例</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当守卫实例为 null 时抛出</exception>
|
||||||
|
public void RemoveGuard(IRouteGuard<TRoute> guard)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(guard);
|
||||||
|
if (_guards.Remove(guard))
|
||||||
|
Log.Debug("Guard removed: {0}", guard.GetType().Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Guard Execution
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行进入守卫检查
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeKey">路由键值</param>
|
||||||
|
/// <param name="context">路由上下文</param>
|
||||||
|
/// <returns>如果所有守卫都允许进入返回 true,否则返回 false</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 守卫按优先级从小到大依次执行。
|
||||||
|
/// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||||
|
/// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||||
|
/// </remarks>
|
||||||
|
protected async Task<bool> ExecuteEnterGuardsAsync(string routeKey, TContext? context)
|
||||||
|
{
|
||||||
|
foreach (var guard in _guards)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, routeKey);
|
||||||
|
var canEnter = await guard.CanEnterAsync(routeKey, context);
|
||||||
|
|
||||||
|
if (!canEnter)
|
||||||
|
{
|
||||||
|
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guard.CanInterrupt)
|
||||||
|
{
|
||||||
|
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||||
|
if (guard.CanInterrupt)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行离开守卫检查
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeKey">路由键值</param>
|
||||||
|
/// <returns>如果所有守卫都允许离开返回 true,否则返回 false</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 守卫按优先级从小到大依次执行。
|
||||||
|
/// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||||
|
/// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。
|
||||||
|
/// </remarks>
|
||||||
|
protected async Task<bool> ExecuteLeaveGuardsAsync(string routeKey)
|
||||||
|
{
|
||||||
|
foreach (var guard in _guards)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, routeKey);
|
||||||
|
var canLeave = await guard.CanLeaveAsync(routeKey);
|
||||||
|
|
||||||
|
if (!canLeave)
|
||||||
|
{
|
||||||
|
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (guard.CanInterrupt)
|
||||||
|
{
|
||||||
|
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
||||||
|
if (guard.CanInterrupt)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Stack Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查栈中是否包含指定路由
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeKey">路由键值</param>
|
||||||
|
/// <returns>如果栈中包含指定路由返回 true,否则返回 false</returns>
|
||||||
|
public bool Contains(string routeKey)
|
||||||
|
{
|
||||||
|
return Stack.Any(r => r.Key == routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取栈顶路由的键值
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>栈顶路由的键值,如果栈为空则返回空字符串</returns>
|
||||||
|
public string PeekKey()
|
||||||
|
{
|
||||||
|
return Stack.Count == 0 ? string.Empty : Stack.Peek().Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断栈顶是否为指定路由
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="routeKey">路由键值</param>
|
||||||
|
/// <returns>如果栈顶是指定路由返回 true,否则返回 false</returns>
|
||||||
|
public bool IsTop(string routeKey)
|
||||||
|
{
|
||||||
|
return Stack.Count != 0 && Stack.Peek().Key.Equals(routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -14,9 +14,9 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Extensions;
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using GFramework.Core.Systems;
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
using GFramework.Game.Abstractions.Enums;
|
||||||
using GFramework.Game.Abstractions.Scene;
|
using GFramework.Game.Abstractions.Scene;
|
||||||
|
using GFramework.Game.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.Scene;
|
namespace GFramework.Game.Scene;
|
||||||
|
|
||||||
@ -25,15 +25,13 @@ namespace GFramework.Game.Scene;
|
|||||||
/// 实现了 <see cref="ISceneRouter"/> 接口,用于管理场景的加载、替换和卸载操作。
|
/// 实现了 <see cref="ISceneRouter"/> 接口,用于管理场景的加载、替换和卸载操作。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class SceneRouterBase
|
public abstract class SceneRouterBase
|
||||||
: AbstractSystem, ISceneRouter
|
: RouterBase<ISceneBehavior, ISceneEnterParam>, ISceneRouter
|
||||||
{
|
{
|
||||||
private static readonly ILogger Log =
|
private static readonly ILogger Log =
|
||||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase));
|
LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase));
|
||||||
|
|
||||||
private readonly List<ISceneRouteGuard> _guards = new();
|
|
||||||
private readonly SceneTransitionPipeline _pipeline = new();
|
private readonly SceneTransitionPipeline _pipeline = new();
|
||||||
|
|
||||||
private readonly Stack<ISceneBehavior> _stack = new();
|
|
||||||
private readonly SemaphoreSlim _transitionLock = new(1, 1);
|
private readonly SemaphoreSlim _transitionLock = new(1, 1);
|
||||||
private ISceneFactory _factory = null!;
|
private ISceneFactory _factory = null!;
|
||||||
|
|
||||||
@ -45,17 +43,17 @@ public abstract class SceneRouterBase
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前场景行为对象。
|
/// 获取当前场景行为对象。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ISceneBehavior? Current => _stack.Count > 0 ? _stack.Peek() : null;
|
public new ISceneBehavior? Current => Stack.Count > 0 ? Stack.Peek() : null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前场景的键名。
|
/// 获取当前场景的键名。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CurrentKey => Current?.Key;
|
public new string? CurrentKey => Current?.Key;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取场景栈的只读视图,按压入顺序排列(从栈底到栈顶)。
|
/// 获取场景栈的只读视图,按压入顺序排列(从栈底到栈顶)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ISceneBehavior> Stack => _stack.Reverse();
|
IEnumerable<ISceneBehavior> ISceneRouter.Stack => base.Stack.Reverse();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取是否正在进行场景转换。
|
/// 获取是否正在进行场景转换。
|
||||||
@ -115,9 +113,9 @@ public abstract class SceneRouterBase
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sceneKey">场景键名。</param>
|
/// <param name="sceneKey">场景键名。</param>
|
||||||
/// <returns>如果场景在栈中返回true,否则返回false。</returns>
|
/// <returns>如果场景在栈中返回true,否则返回false。</returns>
|
||||||
public bool Contains(string sceneKey)
|
public new bool Contains(string sceneKey)
|
||||||
{
|
{
|
||||||
return _stack.Any(s => s.Key == sceneKey);
|
return Stack.Any(s => s.Key == sceneKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -163,46 +161,10 @@ public abstract class SceneRouterBase
|
|||||||
_pipeline.UnregisterAroundHandler(handler);
|
_pipeline.UnregisterAroundHandler(handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加场景路由守卫。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard">守卫实例。</param>
|
|
||||||
public void AddGuard(ISceneRouteGuard guard)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(guard);
|
|
||||||
if (!_guards.Contains(guard))
|
|
||||||
{
|
|
||||||
_guards.Add(guard);
|
|
||||||
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
|
||||||
Log.Debug("Guard added: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 添加场景路由守卫(泛型版本)。
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">守卫类型。</typeparam>
|
|
||||||
public void AddGuard<T>() where T : ISceneRouteGuard, new()
|
|
||||||
{
|
|
||||||
AddGuard(new T());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 移除场景路由守卫。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard">守卫实例。</param>
|
|
||||||
public void RemoveGuard(ISceneRouteGuard guard)
|
|
||||||
{
|
|
||||||
if (_guards.Remove(guard))
|
|
||||||
{
|
|
||||||
Log.Debug("Guard removed: {0}", guard.GetType().Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册场景过渡处理器的抽象方法,由子类实现。
|
/// 注册场景过渡处理器的抽象方法,由子类实现。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract void RegisterHandlers();
|
protected override abstract void RegisterHandlers();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 系统初始化方法,获取场景工厂并注册处理器。
|
/// 系统初始化方法,获取场景工厂并注册处理器。
|
||||||
@ -281,20 +243,20 @@ public abstract class SceneRouterBase
|
|||||||
await scene.OnLoadAsync(param);
|
await scene.OnLoadAsync(param);
|
||||||
|
|
||||||
// 暂停当前场景
|
// 暂停当前场景
|
||||||
if (_stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var current = _stack.Peek();
|
var current = Stack.Peek();
|
||||||
await current.OnPauseAsync();
|
await current.OnPauseAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 压入栈
|
// 压入栈
|
||||||
_stack.Push(scene);
|
Stack.Push(scene);
|
||||||
|
|
||||||
// 进入场景
|
// 进入场景
|
||||||
await scene.OnEnterAsync();
|
await scene.OnEnterAsync();
|
||||||
|
|
||||||
Log.Debug("Push Scene: {0}, stackCount={1}",
|
Log.Debug("Push Scene: {0}, stackCount={1}",
|
||||||
sceneKey, _stack.Count);
|
sceneKey, Stack.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -335,10 +297,10 @@ public abstract class SceneRouterBase
|
|||||||
/// <returns>异步任务。</returns>
|
/// <returns>异步任务。</returns>
|
||||||
private async ValueTask PopInternalAsync()
|
private async ValueTask PopInternalAsync()
|
||||||
{
|
{
|
||||||
if (_stack.Count == 0)
|
if (Stack.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var top = _stack.Peek();
|
var top = Stack.Peek();
|
||||||
|
|
||||||
// 守卫检查
|
// 守卫检查
|
||||||
if (!await ExecuteLeaveGuardsAsync(top.Key))
|
if (!await ExecuteLeaveGuardsAsync(top.Key))
|
||||||
@ -347,7 +309,7 @@ public abstract class SceneRouterBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_stack.Pop();
|
Stack.Pop();
|
||||||
|
|
||||||
// 退出场景
|
// 退出场景
|
||||||
await top.OnExitAsync();
|
await top.OnExitAsync();
|
||||||
@ -359,13 +321,13 @@ public abstract class SceneRouterBase
|
|||||||
Root!.RemoveScene(top);
|
Root!.RemoveScene(top);
|
||||||
|
|
||||||
// 恢复下一个场景
|
// 恢复下一个场景
|
||||||
if (_stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var next = _stack.Peek();
|
var next = Stack.Peek();
|
||||||
await next.OnResumeAsync();
|
await next.OnResumeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug("Pop Scene, stackCount={0}", _stack.Count);
|
Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -406,7 +368,7 @@ public abstract class SceneRouterBase
|
|||||||
/// <returns>异步任务。</returns>
|
/// <returns>异步任务。</returns>
|
||||||
private async ValueTask ClearInternalAsync()
|
private async ValueTask ClearInternalAsync()
|
||||||
{
|
{
|
||||||
while (_stack.Count > 0)
|
while (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
await PopInternalAsync();
|
await PopInternalAsync();
|
||||||
}
|
}
|
||||||
@ -460,82 +422,5 @@ public abstract class SceneRouterBase
|
|||||||
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
|
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行进入场景的守卫检查。
|
|
||||||
/// 按优先级顺序执行所有守卫的CanEnterAsync方法。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneKey">场景键名。</param>
|
|
||||||
/// <param name="param">进入参数。</param>
|
|
||||||
/// <returns>如果所有守卫都允许进入返回true,否则返回false。</returns>
|
|
||||||
private async Task<bool> ExecuteEnterGuardsAsync(string sceneKey, ISceneEnterParam? param)
|
|
||||||
{
|
|
||||||
foreach (var guard in _guards)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, sceneKey);
|
|
||||||
var canEnter = await guard.CanEnterAsync(sceneKey, param);
|
|
||||||
|
|
||||||
if (!canEnter)
|
|
||||||
{
|
|
||||||
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
{
|
|
||||||
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行离开场景的守卫检查。
|
|
||||||
/// 按优先级顺序执行所有守卫的CanLeaveAsync方法。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="sceneKey">场景键名。</param>
|
|
||||||
/// <returns>如果所有守卫都允许离开返回true,否则返回false。</returns>
|
|
||||||
private async Task<bool> ExecuteLeaveGuardsAsync(string sceneKey)
|
|
||||||
{
|
|
||||||
foreach (var guard in _guards)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, sceneKey);
|
|
||||||
var canLeave = await guard.CanLeaveAsync(sceneKey);
|
|
||||||
|
|
||||||
if (!canLeave)
|
|
||||||
{
|
|
||||||
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
{
|
|
||||||
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Extensions;
|
using GFramework.Core.Extensions;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using GFramework.Core.Systems;
|
|
||||||
using GFramework.Game.Abstractions.Enums;
|
using GFramework.Game.Abstractions.Enums;
|
||||||
using GFramework.Game.Abstractions.UI;
|
using GFramework.Game.Abstractions.UI;
|
||||||
|
using GFramework.Game.Routing;
|
||||||
|
|
||||||
namespace GFramework.Game.UI;
|
namespace GFramework.Game.UI;
|
||||||
|
|
||||||
@ -11,15 +11,10 @@ namespace GFramework.Game.UI;
|
|||||||
/// UI路由基类,提供页面栈管理和层级UI管理功能
|
/// UI路由基类,提供页面栈管理和层级UI管理功能
|
||||||
/// 负责UI页面的导航、显示、隐藏以及生命周期管理
|
/// 负责UI页面的导航、显示、隐藏以及生命周期管理
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterParam>, IUiRouter
|
||||||
{
|
{
|
||||||
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(UiRouterBase));
|
private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(UiRouterBase));
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 路由守卫列表,用于控制UI页面的进入和离开
|
|
||||||
/// </summary>
|
|
||||||
private readonly List<IUiRouteGuard> _guards = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 层级管理字典(非栈层级),用于管理Overlay、Modal、Toast等浮层UI
|
/// 层级管理字典(非栈层级),用于管理Overlay、Modal、Toast等浮层UI
|
||||||
/// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典
|
/// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典
|
||||||
@ -31,11 +26,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly UiTransitionPipeline _pipeline = new();
|
private readonly UiTransitionPipeline _pipeline = new();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 页面栈,用于管理UI页面的显示顺序和导航历史
|
|
||||||
/// </summary>
|
|
||||||
private readonly Stack<IUiPageBehavior> _stack = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UI工厂实例,用于创建UI页面和相关对象
|
/// UI工厂实例,用于创建UI页面和相关对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -98,7 +88,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
|
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
|
||||||
Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
|
Log.Debug("Push UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count);
|
||||||
|
|
||||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||||
{
|
{
|
||||||
@ -126,7 +116,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
}
|
}
|
||||||
|
|
||||||
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
|
var @event = CreateEvent(uiKey, UiTransitionType.Push, policy, param);
|
||||||
Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, _stack.Count);
|
Log.Debug("Push existing UI Page: key={0}, policy={1}, stackBefore={2}", uiKey, policy, Stack.Count);
|
||||||
|
|
||||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||||
{
|
{
|
||||||
@ -142,13 +132,13 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// <param name="policy">页面弹出策略</param>
|
/// <param name="policy">页面弹出策略</param>
|
||||||
public async ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy)
|
public async ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy)
|
||||||
{
|
{
|
||||||
if (_stack.Count == 0)
|
if (Stack.Count == 0)
|
||||||
{
|
{
|
||||||
Log.Debug("Pop ignored: stack is empty");
|
Log.Debug("Pop ignored: stack is empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var leavingUiKey = _stack.Peek().Key;
|
var leavingUiKey = Stack.Peek().Key;
|
||||||
|
|
||||||
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
|
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
|
||||||
{
|
{
|
||||||
@ -156,7 +146,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextUiKey = _stack.Count > 1 ? _stack.ElementAt(1).Key : null;
|
var nextUiKey = Stack.Count > 1 ? Stack.ElementAt(1).Key : null;
|
||||||
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
|
var @event = CreateEvent(nextUiKey, UiTransitionType.Pop);
|
||||||
|
|
||||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||||
@ -226,7 +216,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
public async ValueTask ClearAsync()
|
public async ValueTask ClearAsync()
|
||||||
{
|
{
|
||||||
var @event = CreateEvent(string.Empty, UiTransitionType.Clear);
|
var @event = CreateEvent(string.Empty, UiTransitionType.Clear);
|
||||||
Log.Debug("Clear UI Stack, stackCount={0}", _stack.Count);
|
Log.Debug("Clear UI Stack, stackCount={0}", Stack.Count);
|
||||||
|
|
||||||
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
await _pipeline.ExecuteAroundAsync(@event, async () =>
|
||||||
{
|
{
|
||||||
@ -240,9 +230,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// 获取栈顶元素的键值
|
/// 获取栈顶元素的键值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>栈顶UI页面的键值,如果栈为空则返回空字符串</returns>
|
/// <returns>栈顶UI页面的键值,如果栈为空则返回空字符串</returns>
|
||||||
public string PeekKey()
|
public new string PeekKey()
|
||||||
{
|
{
|
||||||
return _stack.Count == 0 ? string.Empty : _stack.Peek().Key;
|
return Stack.Count == 0 ? string.Empty : Stack.Peek().Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -251,7 +241,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// <returns>栈顶UI页面行为实例,如果栈为空则返回null</returns>
|
/// <returns>栈顶UI页面行为实例,如果栈为空则返回null</returns>
|
||||||
public IUiPageBehavior? Peek()
|
public IUiPageBehavior? Peek()
|
||||||
{
|
{
|
||||||
return _stack.Count == 0 ? null : _stack.Peek();
|
return Stack.Count == 0 ? null : Stack.Peek();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -259,9 +249,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uiKey">要检查的UI页面键值</param>
|
/// <param name="uiKey">要检查的UI页面键值</param>
|
||||||
/// <returns>如果栈顶是指定UI则返回true,否则返回false</returns>
|
/// <returns>如果栈顶是指定UI则返回true,否则返回false</returns>
|
||||||
public bool IsTop(string uiKey)
|
public new bool IsTop(string uiKey)
|
||||||
{
|
{
|
||||||
return _stack.Count != 0 && _stack.Peek().Key.Equals(uiKey);
|
return Stack.Count != 0 && Stack.Peek().Key.Equals(uiKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -269,15 +259,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uiKey">要检查的UI页面键值</param>
|
/// <param name="uiKey">要检查的UI页面键值</param>
|
||||||
/// <returns>如果栈中包含指定UI则返回true,否则返回false</returns>
|
/// <returns>如果栈中包含指定UI则返回true,否则返回false</returns>
|
||||||
public bool Contains(string uiKey)
|
public new bool Contains(string uiKey)
|
||||||
{
|
{
|
||||||
return _stack.Any(p => p.Key.Equals(uiKey));
|
return Stack.Any(p => p.Key.Equals(uiKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取栈深度
|
/// 获取栈深度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Count => _stack.Count;
|
public new int Count => Stack.Count;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -458,51 +448,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Route Guards
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册路由守卫
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard">路由守卫实例</param>
|
|
||||||
/// <exception cref="ArgumentNullException">当守卫实例为null时抛出</exception>
|
|
||||||
public void AddGuard(IUiRouteGuard guard)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(guard);
|
|
||||||
|
|
||||||
if (_guards.Contains(guard))
|
|
||||||
{
|
|
||||||
Log.Debug("Guard already registered: {0}", guard.GetType().Name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_guards.Add(guard);
|
|
||||||
_guards.Sort((a, b) => a.Priority.CompareTo(b.Priority));
|
|
||||||
Log.Debug("Guard registered: {0}, Priority={1}", guard.GetType().Name, guard.Priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册路由守卫(泛型)
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">路由守卫类型,必须实现IUiRouteGuard接口且有无参构造函数</typeparam>
|
|
||||||
public void AddGuard<T>() where T : IUiRouteGuard, new()
|
|
||||||
{
|
|
||||||
AddGuard(new T());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 移除路由守卫
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard">要移除的路由守卫实例</param>
|
|
||||||
/// <exception cref="ArgumentNullException">当守卫实例为null时抛出</exception>
|
|
||||||
public void RemoveGuard(IUiRouteGuard guard)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(guard);
|
|
||||||
if (_guards.Remove(guard))
|
|
||||||
Log.Debug("Guard removed: {0}", guard.GetType().Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Initialization
|
#region Initialization
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -525,7 +470,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// 抽象方法,用于注册具体的处理程序。
|
/// 抽象方法,用于注册具体的处理程序。
|
||||||
/// 子类必须实现此方法以完成特定的处理逻辑注册。
|
/// 子类必须实现此方法以完成特定的处理逻辑注册。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected abstract void RegisterHandlers();
|
protected override abstract void RegisterHandlers();
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -655,9 +600,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// <param name="policy">过渡策略</param>
|
/// <param name="policy">过渡策略</param>
|
||||||
private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy)
|
private void DoPushPageInternal(IUiPageBehavior page, IUiPageEnterParam? param, UiTransitionPolicy policy)
|
||||||
{
|
{
|
||||||
if (_stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var current = _stack.Peek();
|
var current = Stack.Peek();
|
||||||
Log.Debug("Pause current page: {0}", current.View.GetType().Name);
|
Log.Debug("Pause current page: {0}", current.View.GetType().Name);
|
||||||
current.OnPause();
|
current.OnPause();
|
||||||
|
|
||||||
@ -671,9 +616,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
Log.Debug("Add page to UiRoot: {0}", page.View.GetType().Name);
|
Log.Debug("Add page to UiRoot: {0}", page.View.GetType().Name);
|
||||||
_uiRoot.AddUiPage(page);
|
_uiRoot.AddUiPage(page);
|
||||||
|
|
||||||
_stack.Push(page);
|
Stack.Push(page);
|
||||||
|
|
||||||
Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, _stack.Count);
|
Log.Debug("Enter & Show page: {0}, stackAfter={1}", page.View.GetType().Name, Stack.Count);
|
||||||
page.OnEnter(param);
|
page.OnEnter(param);
|
||||||
page.OnShow();
|
page.OnShow();
|
||||||
}
|
}
|
||||||
@ -684,12 +629,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// <param name="policy">页面弹出策略</param>
|
/// <param name="policy">页面弹出策略</param>
|
||||||
private void DoPopInternal(UiPopPolicy policy)
|
private void DoPopInternal(UiPopPolicy policy)
|
||||||
{
|
{
|
||||||
if (_stack.Count == 0)
|
if (Stack.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var top = _stack.Pop();
|
var top = Stack.Pop();
|
||||||
Log.Debug("Pop UI Page internal: {0}, policy={1}, stackAfterPop={2}",
|
Log.Debug("Pop UI Page internal: {0}, policy={1}, stackAfterPop={2}",
|
||||||
top.GetType().Name, policy, _stack.Count);
|
top.GetType().Name, policy, Stack.Count);
|
||||||
|
|
||||||
if (policy == UiPopPolicy.Destroy)
|
if (policy == UiPopPolicy.Destroy)
|
||||||
{
|
{
|
||||||
@ -701,9 +646,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
top.OnHide();
|
top.OnHide();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_stack.Count > 0)
|
if (Stack.Count > 0)
|
||||||
{
|
{
|
||||||
var next = _stack.Peek();
|
var next = Stack.Peek();
|
||||||
next.OnResume();
|
next.OnResume();
|
||||||
next.OnShow();
|
next.OnShow();
|
||||||
}
|
}
|
||||||
@ -715,85 +660,10 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
|
|||||||
/// <param name="policy">页面弹出策略</param>
|
/// <param name="policy">页面弹出策略</param>
|
||||||
private void DoClearInternal(UiPopPolicy policy)
|
private void DoClearInternal(UiPopPolicy policy)
|
||||||
{
|
{
|
||||||
Log.Debug("Clear UI Stack internal, count={0}", _stack.Count);
|
Log.Debug("Clear UI Stack internal, count={0}", Stack.Count);
|
||||||
while (_stack.Count > 0)
|
while (Stack.Count > 0)
|
||||||
DoPopInternal(policy);
|
DoPopInternal(policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行进入守卫检查
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uiKey">UI页面键值</param>
|
|
||||||
/// <param name="param">页面进入参数</param>
|
|
||||||
/// <returns>如果允许进入则返回true,否则返回false</returns>
|
|
||||||
private async Task<bool> ExecuteEnterGuardsAsync(string uiKey, IUiPageEnterParam? param)
|
|
||||||
{
|
|
||||||
foreach (var guard in _guards)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, uiKey);
|
|
||||||
var canEnter = await guard.CanEnterAsync(uiKey, param);
|
|
||||||
|
|
||||||
if (!canEnter)
|
|
||||||
{
|
|
||||||
Log.Debug("Enter guard blocked: {0}", guard.GetType().Name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
{
|
|
||||||
Log.Debug("Enter guard {0} passed, can interrupt = true", guard.GetType().Name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error("Enter guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 执行离开守卫检查
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uiKey">UI页面键值</param>
|
|
||||||
/// <returns>如果允许离开则返回true,否则返回false</returns>
|
|
||||||
private async Task<bool> ExecuteLeaveGuardsAsync(string uiKey)
|
|
||||||
{
|
|
||||||
foreach (var guard in _guards)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, uiKey);
|
|
||||||
var canLeave = await guard.CanLeaveAsync(uiKey);
|
|
||||||
|
|
||||||
if (!canLeave)
|
|
||||||
{
|
|
||||||
Log.Debug("Leave guard blocked: {0}", guard.GetType().Name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
{
|
|
||||||
Log.Debug("Leave guard {0} passed, can interrupt = true", guard.GetType().Name);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Error("Leave guard {0} failed: {1}", guard.GetType().Name, ex.Message);
|
|
||||||
if (guard.CanInterrupt)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@ -55,6 +55,7 @@
|
|||||||
<None Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
<None Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
||||||
<None Remove="GFramework.SourceGenerators.Attributes\**"/>
|
<None Remove="GFramework.SourceGenerators.Attributes\**"/>
|
||||||
<None Remove="Godot\**"/>
|
<None Remove="Godot\**"/>
|
||||||
|
<None Remove="GFramework.Game.Tests\**"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- 聚合核心模块 -->
|
<!-- 聚合核心模块 -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -93,6 +94,7 @@
|
|||||||
<Compile Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
<Compile Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
||||||
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
|
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
|
||||||
<Compile Remove="Godot\**"/>
|
<Compile Remove="Godot\**"/>
|
||||||
|
<Compile Remove="GFramework.Game.Tests\**"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Remove="GFramework.Core\**"/>
|
<EmbeddedResource Remove="GFramework.Core\**"/>
|
||||||
@ -117,6 +119,7 @@
|
|||||||
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
|
||||||
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
|
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
|
||||||
<EmbeddedResource Remove="Godot\**"/>
|
<EmbeddedResource Remove="Godot\**"/>
|
||||||
|
<EmbeddedResource Remove="GFramework.Game.Tests\**"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
|
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>
|
||||||
|
|||||||
@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch", "GFra
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch.Tests", "GFramework.Ecs.Arch.Tests\GFramework.Ecs.Arch.Tests.csproj", "{112CF413-4596-4AA3-B3FE-65532802FDD6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch.Tests", "GFramework.Ecs.Arch.Tests\GFramework.Ecs.Arch.Tests.csproj", "{112CF413-4596-4AA3-B3FE-65532802FDD6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GFramework.Game.Tests\GFramework.Game.Tests.csproj", "{738DC58A-0387-4D75-AA96-1C1D8C29D350}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -234,6 +236,18 @@ Global
|
|||||||
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x64.Build.0 = Release|Any CPU
|
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.ActiveCfg = Release|Any CPU
|
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.Build.0 = Release|Any CPU
|
{112CF413-4596-4AA3-B3FE-65532802FDD6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{738DC58A-0387-4D75-AA96-1C1D8C29D350}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user