refactor(game): 重构路由系统并优化CI测试流程

- 将SceneRouterBase和UiRouterBase继承自新的RouterBase基类
- 移除原有的守卫管理相关代码,统一使用基类实现
- 更新路由栈操作使用基类提供的Stack属性
- 重写Current、Contains等方法以使用基类实现
- 在CI工作流中启用并发测试执行以提升性能
- 添加等待步骤确保并发测试完成
- 更新项目文件排除测试项目的编译
- 在解决方案文件中添加GFramework.Game.Tests项目引用
- 新增RouterBase基类提供通用路由管理功能
This commit is contained in:
GeWuYou 2026-03-17 15:01:55 +08:00
parent 27858df94e
commit 4afa856fdc
19 changed files with 1080 additions and 358 deletions

View File

@ -113,29 +113,42 @@ jobs:
run: dotnet build -c Release --no-restore
# 运行单元测试输出TRX格式结果到TestResults目录
# 使用并发执行以加快测试速度
- name: Test - Core
run: |
dotnet test GFramework.Core.Tests \
-c Release \
--no-build \
--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
run: |
dotnet test GFramework.SourceGenerators.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=sg-$RANDOM.trx" \
--results-directory TestResults
- name: Test - GFramework.Ecs.Arch.Tests
--results-directory TestResults &
- name: Test - ECS Arch
run: |
dotnet test GFramework.Ecs.Arch.Tests \
-c Release \
--no-build \
--logger "trx;LogFileName=ecs-arch-$RANDOM.trx" \
--results-directory TestResults
--results-directory TestResults &
# 等待所有并发测试完成
- name: Wait for tests
run: wait
- name: Generate CTRF report
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

@ -2,22 +2,24 @@
// 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;
namespace GFramework.Game.Abstractions.Scene;
/// <summary>
/// 场景行为接口,定义了场景生命周期管理的标准方法。
/// 实现此接口的类需要处理场景的加载、激活、暂停、恢复和卸载等核心操作。
/// </summary>
public interface ISceneBehavior
public interface ISceneBehavior : IRoute
{
/// <summary>
/// 获取场景的唯一标识符。

View File

@ -2,19 +2,21 @@
// 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;
namespace GFramework.Game.Abstractions.Scene;
/// <summary>
/// 场景进入参数接口
/// 该接口用于定义场景跳转时传递的参数数据结构
/// </summary>
public interface ISceneEnterParam;
public interface ISceneEnterParam : IRouteContext;

View File

@ -11,28 +11,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using GFramework.Game.Abstractions.Routing;
namespace GFramework.Game.Abstractions.Scene;
/// <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>
@ -46,5 +34,5 @@ public interface ISceneRouteGuard
/// </summary>
/// <param name="sceneKey">当前场景的唯一标识符。</param>
/// <returns>如果允许离开则返回 true否则返回 false。</returns>
Task<bool> CanLeaveAsync(string sceneKey);
new Task<bool> CanLeaveAsync(string sceneKey);
}

View File

@ -1,11 +1,12 @@
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.Routing;
namespace GFramework.Game.Abstractions.UI;
/// <summary>
/// UI页面行为接口定义了UI页面的生命周期方法和状态管理
/// </summary>
public interface IUiPageBehavior
public interface IUiPageBehavior : IRoute
{
/// <summary>
/// 获取或设置当前UI句柄。

View File

@ -1,7 +1,9 @@
namespace GFramework.Game.Abstractions.UI;
using GFramework.Game.Abstractions.Routing;
namespace GFramework.Game.Abstractions.UI;
/// <summary>
/// UI页面进入参数接口
/// 该接口用于定义UI页面跳转时传递的参数数据结构
/// </summary>
public interface IUiPageEnterParam;
public interface IUiPageEnterParam : IRouteContext;

View File

@ -1,22 +1,13 @@
using GFramework.Game.Abstractions.Routing;
namespace GFramework.Game.Abstractions.UI;
/// <summary>
/// UI路由守卫接口
/// 用于拦截和处理UI路由切换实现业务逻辑解耦
/// </summary>
public interface IUiRouteGuard
public interface IUiRouteGuard : IRouteGuard<IUiPageBehavior>
{
/// <summary>
/// 守卫优先级,数值越小越先执行
/// </summary>
int Priority { get; }
/// <summary>
/// 是否可中断后续守卫
/// 如果返回 true当该守卫返回 false 时,将停止执行后续守卫
/// </summary>
bool CanInterrupt { get; }
/// <summary>
/// 进入UI前的检查
/// </summary>
@ -30,5 +21,5 @@ public interface IUiRouteGuard
/// </summary>
/// <param name="uiKey">当前UI标识符</param>
/// <returns>true表示允许离开false表示拦截</returns>
Task<bool> CanLeaveAsync(string uiKey);
new Task<bool> CanLeaveAsync(string uiKey);
}

View File

@ -113,28 +113,6 @@ public interface IUiRouter : ISystem
/// </summary>
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
/// <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.Extensions;
using GFramework.Core.Logging;
using GFramework.Core.Systems;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.Scene;
using GFramework.Game.Routing;
namespace GFramework.Game.Scene;
@ -25,15 +25,13 @@ namespace GFramework.Game.Scene;
/// 实现了 <see cref="ISceneRouter"/> 接口,用于管理场景的加载、替换和卸载操作。
/// </summary>
public abstract class SceneRouterBase
: AbstractSystem, ISceneRouter
: RouterBase<ISceneBehavior, ISceneEnterParam>, ISceneRouter
{
private static readonly ILogger Log =
LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase));
private readonly List<ISceneRouteGuard> _guards = new();
private readonly SceneTransitionPipeline _pipeline = new();
private readonly Stack<ISceneBehavior> _stack = new();
private readonly SemaphoreSlim _transitionLock = new(1, 1);
private ISceneFactory _factory = null!;
@ -45,17 +43,17 @@ public abstract class SceneRouterBase
/// <summary>
/// 获取当前场景行为对象。
/// </summary>
public ISceneBehavior? Current => _stack.Count > 0 ? _stack.Peek() : null;
public new ISceneBehavior? Current => Stack.Count > 0 ? Stack.Peek() : null;
/// <summary>
/// 获取当前场景的键名。
/// </summary>
public string? CurrentKey => Current?.Key;
public new string? CurrentKey => Current?.Key;
/// <summary>
/// 获取场景栈的只读视图,按压入顺序排列(从栈底到栈顶)。
/// </summary>
public IEnumerable<ISceneBehavior> Stack => _stack.Reverse();
IEnumerable<ISceneBehavior> ISceneRouter.Stack => base.Stack.Reverse();
/// <summary>
/// 获取是否正在进行场景转换。
@ -115,9 +113,9 @@ public abstract class SceneRouterBase
/// </summary>
/// <param name="sceneKey">场景键名。</param>
/// <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
@ -163,46 +161,10 @@ public abstract class SceneRouterBase
_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>
protected abstract void RegisterHandlers();
protected override abstract void RegisterHandlers();
/// <summary>
/// 系统初始化方法,获取场景工厂并注册处理器。
@ -281,20 +243,20 @@ public abstract class SceneRouterBase
await scene.OnLoadAsync(param);
// 暂停当前场景
if (_stack.Count > 0)
if (Stack.Count > 0)
{
var current = _stack.Peek();
var current = Stack.Peek();
await current.OnPauseAsync();
}
// 压入栈
_stack.Push(scene);
Stack.Push(scene);
// 进入场景
await scene.OnEnterAsync();
Log.Debug("Push Scene: {0}, stackCount={1}",
sceneKey, _stack.Count);
sceneKey, Stack.Count);
}
#endregion
@ -335,10 +297,10 @@ public abstract class SceneRouterBase
/// <returns>异步任务。</returns>
private async ValueTask PopInternalAsync()
{
if (_stack.Count == 0)
if (Stack.Count == 0)
return;
var top = _stack.Peek();
var top = Stack.Peek();
// 守卫检查
if (!await ExecuteLeaveGuardsAsync(top.Key))
@ -347,7 +309,7 @@ public abstract class SceneRouterBase
return;
}
_stack.Pop();
Stack.Pop();
// 退出场景
await top.OnExitAsync();
@ -359,13 +321,13 @@ public abstract class SceneRouterBase
Root!.RemoveScene(top);
// 恢复下一个场景
if (_stack.Count > 0)
if (Stack.Count > 0)
{
var next = _stack.Peek();
var next = Stack.Peek();
await next.OnResumeAsync();
}
Log.Debug("Pop Scene, stackCount={0}", _stack.Count);
Log.Debug("Pop Scene, stackCount={0}", Stack.Count);
}
#endregion
@ -406,7 +368,7 @@ public abstract class SceneRouterBase
/// <returns>异步任务。</returns>
private async ValueTask ClearInternalAsync()
{
while (_stack.Count > 0)
while (Stack.Count > 0)
{
await PopInternalAsync();
}
@ -460,82 +422,5 @@ public abstract class SceneRouterBase
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
}

View File

@ -1,9 +1,9 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Extensions;
using GFramework.Core.Logging;
using GFramework.Core.Systems;
using GFramework.Game.Abstractions.Enums;
using GFramework.Game.Abstractions.UI;
using GFramework.Game.Routing;
namespace GFramework.Game.UI;
@ -11,15 +11,10 @@ namespace GFramework.Game.UI;
/// UI路由基类提供页面栈管理和层级UI管理功能
/// 负责UI页面的导航、显示、隐藏以及生命周期管理
/// </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));
/// <summary>
/// 路由守卫列表用于控制UI页面的进入和离开
/// </summary>
private readonly List<IUiRouteGuard> _guards = new();
/// <summary>
/// 层级管理字典非栈层级用于管理Overlay、Modal、Toast等浮层UI
/// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典
@ -31,11 +26,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// </summary>
private readonly UiTransitionPipeline _pipeline = new();
/// <summary>
/// 页面栈用于管理UI页面的显示顺序和导航历史
/// </summary>
private readonly Stack<IUiPageBehavior> _stack = new();
/// <summary>
/// UI工厂实例用于创建UI页面和相关对象
/// </summary>
@ -98,7 +88,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
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 () =>
{
@ -126,7 +116,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
}
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 () =>
{
@ -142,13 +132,13 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// <param name="policy">页面弹出策略</param>
public async ValueTask PopAsync(UiPopPolicy policy = UiPopPolicy.Destroy)
{
if (_stack.Count == 0)
if (Stack.Count == 0)
{
Log.Debug("Pop ignored: stack is empty");
return;
}
var leavingUiKey = _stack.Peek().Key;
var leavingUiKey = Stack.Peek().Key;
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
{
@ -156,7 +146,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
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);
await _pipeline.ExecuteAroundAsync(@event, async () =>
@ -226,7 +216,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
public async ValueTask ClearAsync()
{
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 () =>
{
@ -240,9 +230,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 获取栈顶元素的键值
/// </summary>
/// <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>
@ -251,7 +241,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// <returns>栈顶UI页面行为实例如果栈为空则返回null</returns>
public IUiPageBehavior? Peek()
{
return _stack.Count == 0 ? null : _stack.Peek();
return Stack.Count == 0 ? null : Stack.Peek();
}
/// <summary>
@ -259,9 +249,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// </summary>
/// <param name="uiKey">要检查的UI页面键值</param>
/// <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>
@ -269,15 +259,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// </summary>
/// <param name="uiKey">要检查的UI页面键值</param>
/// <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>
public int Count => _stack.Count;
public new int Count => Stack.Count;
#endregion
@ -458,51 +448,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
#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
/// <summary>
@ -525,7 +470,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// 抽象方法,用于注册具体的处理程序。
/// 子类必须实现此方法以完成特定的处理逻辑注册。
/// </summary>
protected abstract void RegisterHandlers();
protected override abstract void RegisterHandlers();
#endregion
@ -655,9 +600,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// <param name="policy">过渡策略</param>
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);
current.OnPause();
@ -671,9 +616,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
Log.Debug("Add page to UiRoot: {0}", page.View.GetType().Name);
_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.OnShow();
}
@ -684,12 +629,12 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// <param name="policy">页面弹出策略</param>
private void DoPopInternal(UiPopPolicy policy)
{
if (_stack.Count == 0)
if (Stack.Count == 0)
return;
var top = _stack.Pop();
var top = Stack.Pop();
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)
{
@ -701,9 +646,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
top.OnHide();
}
if (_stack.Count > 0)
if (Stack.Count > 0)
{
var next = _stack.Peek();
var next = Stack.Peek();
next.OnResume();
next.OnShow();
}
@ -715,85 +660,10 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter
/// <param name="policy">页面弹出策略</param>
private void DoClearInternal(UiPopPolicy policy)
{
Log.Debug("Clear UI Stack internal, count={0}", _stack.Count);
while (_stack.Count > 0)
Log.Debug("Clear UI Stack internal, count={0}", Stack.Count);
while (Stack.Count > 0)
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
}

View File

@ -55,6 +55,7 @@
<None Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
<None Remove="GFramework.SourceGenerators.Attributes\**"/>
<None Remove="Godot\**"/>
<None Remove="GFramework.Game.Tests\**"/>
</ItemGroup>
<!-- 聚合核心模块 -->
<ItemGroup>
@ -93,6 +94,7 @@
<Compile Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
<Compile Remove="GFramework.SourceGenerators.Attributes\**"/>
<Compile Remove="Godot\**"/>
<Compile Remove="GFramework.Game.Tests\**"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="GFramework.Core\**"/>
@ -117,6 +119,7 @@
<EmbeddedResource Remove="GFramework.Godot.SourceGenerators.Attributes\**"/>
<EmbeddedResource Remove="GFramework.SourceGenerators.Attributes\**"/>
<EmbeddedResource Remove="Godot\**"/>
<EmbeddedResource Remove="GFramework.Game.Tests\**"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md"/>

View File

@ -32,6 +32,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Ecs.Arch", "GFra
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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.Tests", "GFramework.Game.Tests\GFramework.Game.Tests.csproj", "{738DC58A-0387-4D75-AA96-1C1D8C29D350}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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|x86.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE