Merge pull request #113 from GeWuYou/refactor/router-base-ci-concurrency

refactor(game): 重构路由系统并优化CI测试流程
This commit is contained in:
gewuyou 2026-03-17 16:45:05 +08:00 committed by GitHub
commit 2accbf4bdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1107 additions and 376 deletions

View File

@ -13,8 +13,9 @@ permissions:
security-events: write security-events: write
jobs: jobs:
test: # 代码质量检查 job并行执行不阻塞构建
name: Build and Test code-quality:
name: Code Quality & Security
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -23,9 +24,11 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
# 校验C#命名空间与源码目录是否符合命名规范 # 校验C#命名空间与源码目录是否符合命名规范
- name: Validate C# naming - name: Validate C# naming
run: bash scripts/validate-csharp-naming.sh run: bash scripts/validate-csharp-naming.sh
# 缓存MegaLinter # 缓存MegaLinter
- name: Cache MegaLinter - name: Cache MegaLinter
uses: actions/cache@v5 uses: actions/cache@v5
@ -35,7 +38,6 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-megalinter- ${{ runner.os }}-megalinter-
# MegaLinter扫描步骤 # MegaLinter扫描步骤
# 执行代码质量检查和安全扫描生成SARIF格式报告 # 执行代码质量检查和安全扫描生成SARIF格式报告
- name: MegaLinter - name: MegaLinter
@ -44,11 +46,13 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }} FAIL_ON_ERROR: ${{ github.ref == 'refs/heads/main' }}
# 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心 # 上传SARIF格式的安全和代码质量问题报告到GitHub安全中心
- name: Upload SARIF - name: Upload SARIF
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@v4
with: with:
sarif_file: megalinter-reports/sarif sarif_file: megalinter-reports/sarif
# 缓存TruffleHog # 缓存TruffleHog
- name: Cache TruffleHog - name: Cache TruffleHog
uses: actions/cache@v5 uses: actions/cache@v5
@ -69,6 +73,18 @@ jobs:
# 当前提交哈希,作为扫描的目标版本 # 当前提交哈希,作为扫描的目标版本
head: ${{ github.sha }} head: ${{ github.sha }}
# 构建和测试 job并行执行
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
steps:
# 检出源代码
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
# 安装和配置.NET SDK版本 # 安装和配置.NET SDK版本
- name: Setup .NET 8 - name: Setup .NET 8
uses: actions/setup-dotnet@v5 uses: actions/setup-dotnet@v5
@ -113,29 +129,35 @@ jobs:
run: dotnet build -c Release --no-restore run: dotnet build -c Release --no-restore
# 运行单元测试输出TRX格式结果到TestResults目录 # 运行单元测试输出TRX格式结果到TestResults目录
- name: Test - Core # 在同一个 step 中并发执行所有测试以加快速度
- name: Test All Projects
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 &
dotnet test GFramework.Game.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=game-$RANDOM.trx" \
--results-directory TestResults &
- name: Test - SourceGenerators
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
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 &
# 等待所有后台测试完成
wait
- name: Generate CTRF report - name: Generate CTRF report
run: | run: |

View 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; }
}

View 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
{
// 标记接口,用于类型约束
}

View 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);
}

View File

@ -11,13 +11,15 @@
// 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>
/// 获取场景的唯一标识符。 /// 获取场景的唯一标识符。

View File

@ -11,10 +11,12 @@
// 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;

View File

@ -11,40 +11,28 @@
// 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>
/// <param name="sceneKey">目标场景的唯一标识符。</param> /// <param name="sceneKey">目标场景的唯一标识符。</param>
/// <param name="param">场景进入参数,可能包含初始化数据或上下文信息。</param> /// <param name="param">场景进入参数,可能包含初始化数据或上下文信息。</param>
/// <returns>如果允许进入则返回 true否则返回 false。</returns> /// <returns>如果允许进入则返回 true否则返回 false。</returns>
Task<bool> CanEnterAsync(string sceneKey, ISceneEnterParam? param); ValueTask<bool> CanEnterAsync(string sceneKey, ISceneEnterParam? param);
/// <summary> /// <summary>
/// 异步检查是否允许离开指定场景。 /// 异步检查是否允许离开指定场景。
/// </summary> /// </summary>
/// <param name="sceneKey">当前场景的唯一标识符。</param> /// <param name="sceneKey">当前场景的唯一标识符。</param>
/// <returns>如果允许离开则返回 true否则返回 false。</returns> /// <returns>如果允许离开则返回 true否则返回 false。</returns>
Task<bool> CanLeaveAsync(string sceneKey); ValueTask<bool> CanLeaveAsync(string sceneKey);
} }

View File

@ -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句柄。

View File

@ -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;

View File

@ -1,34 +1,25 @@
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>
/// <param name="uiKey">目标UI标识符</param> /// <param name="uiKey">目标UI标识符</param>
/// <param name="param">进入参数</param> /// <param name="param">进入参数</param>
/// <returns>true表示允许进入false表示拦截</returns> /// <returns>true表示允许进入false表示拦截</returns>
Task<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param); ValueTask<bool> CanEnterAsync(string uiKey, IUiPageEnterParam? param);
/// <summary> /// <summary>
/// 离开UI前的检查 /// 离开UI前的检查
/// </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); ValueTask<bool> CanLeaveAsync(string uiKey);
} }

View File

@ -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>

View 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>

View 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;

View 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));
}
}

View 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
}

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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"/>

View File

@ -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