diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df64bd6..ffda3e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | diff --git a/GFramework.Game.Abstractions/Routing/IRoute.cs b/GFramework.Game.Abstractions/Routing/IRoute.cs new file mode 100644 index 0000000..c626273 --- /dev/null +++ b/GFramework.Game.Abstractions/Routing/IRoute.cs @@ -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; + +/// +/// 路由项接口,表示可路由的对象 +/// +public interface IRoute +{ + /// + /// 路由键值,用于唯一标识路由项 + /// + string Key { get; } +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/Routing/IRouteContext.cs b/GFramework.Game.Abstractions/Routing/IRouteContext.cs new file mode 100644 index 0000000..d58aca4 --- /dev/null +++ b/GFramework.Game.Abstractions/Routing/IRouteContext.cs @@ -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; + +/// +/// 路由上下文接口,表示路由进入时的参数 +/// +/// +/// 这是一个标记接口,用于类型约束。 +/// 具体的路由上下文类型应该实现此接口。 +/// +public interface IRouteContext +{ + // 标记接口,用于类型约束 +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/Routing/IRouteGuard.cs b/GFramework.Game.Abstractions/Routing/IRouteGuard.cs new file mode 100644 index 0000000..7e8ce2f --- /dev/null +++ b/GFramework.Game.Abstractions/Routing/IRouteGuard.cs @@ -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; + +/// +/// 路由守卫接口,用于控制路由的进入和离开 +/// +/// 路由项类型 +public interface IRouteGuard where TRoute : IRoute +{ + /// + /// 守卫优先级,数值越小优先级越高 + /// + /// + /// 守卫按优先级从小到大依次执行。 + /// 建议使用 0-100 的范围,默认为 50。 + /// + int Priority { get; } + + /// + /// 是否可以中断后续守卫的执行 + /// + /// + /// 如果为 true,当此守卫返回 true 或抛出异常时,将中断后续守卫的执行。 + /// 如果为 false,将继续执行后续守卫。 + /// + bool CanInterrupt { get; } + + /// + /// 检查是否可以进入指定路由 + /// + /// 路由键值 + /// 路由上下文 + /// 如果允许进入返回 true,否则返回 false + ValueTask CanEnterAsync(string routeKey, IRouteContext? context); + + /// + /// 检查是否可以离开指定路由 + /// + /// 路由键值 + /// 如果允许离开返回 true,否则返回 false + ValueTask CanLeaveAsync(string routeKey); +} \ No newline at end of file diff --git a/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs b/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs index 4a817e9..2bb07ee 100644 --- a/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs +++ b/GFramework.Game.Abstractions/Scene/ISceneBehavior.cs @@ -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; /// /// 场景行为接口,定义了场景生命周期管理的标准方法。 /// 实现此接口的类需要处理场景的加载、激活、暂停、恢复和卸载等核心操作。 /// -public interface ISceneBehavior +public interface ISceneBehavior : IRoute { /// /// 获取场景的唯一标识符。 diff --git a/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs b/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs index d5b807c..2c6453c 100644 --- a/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs +++ b/GFramework.Game.Abstractions/Scene/ISceneEnterParam.cs @@ -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; /// /// 场景进入参数接口 /// 该接口用于定义场景跳转时传递的参数数据结构 /// -public interface ISceneEnterParam; \ No newline at end of file +public interface ISceneEnterParam : IRouteContext; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs b/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs index 5aca883..a98c878 100644 --- a/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs +++ b/GFramework.Game.Abstractions/Scene/ISceneRouteGuard.cs @@ -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; /// /// 场景路由守卫接口,用于在场景切换前进行权限检查和条件验证。 /// 实现此接口可以拦截场景的进入和离开操作。 /// -public interface ISceneRouteGuard +public interface ISceneRouteGuard : IRouteGuard { - /// - /// 获取守卫的执行优先级。 - /// 数值越小优先级越高,越先执行。 - /// 建议范围:-1000 到 1000。 - /// - int Priority { get; } - - /// - /// 获取守卫是否可以中断后续守卫的执行。 - /// true 表示当前守卫通过后,可以跳过后续守卫直接允许操作。 - /// false 表示即使当前守卫通过,仍需执行所有后续守卫。 - /// - bool CanInterrupt { get; } - /// /// 异步检查是否允许进入指定场景。 /// @@ -46,5 +34,5 @@ public interface ISceneRouteGuard /// /// 当前场景的唯一标识符。 /// 如果允许离开则返回 true,否则返回 false。 - Task CanLeaveAsync(string sceneKey); + new Task CanLeaveAsync(string sceneKey); } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs b/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs index efd0e53..8e84067 100644 --- a/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs +++ b/GFramework.Game.Abstractions/UI/IUiPageBehavior.cs @@ -1,11 +1,12 @@ using GFramework.Game.Abstractions.Enums; +using GFramework.Game.Abstractions.Routing; namespace GFramework.Game.Abstractions.UI; /// /// UI页面行为接口,定义了UI页面的生命周期方法和状态管理 /// -public interface IUiPageBehavior +public interface IUiPageBehavior : IRoute { /// /// 获取或设置当前UI句柄。 diff --git a/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs b/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs index 29bea63..b0b0351 100644 --- a/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs +++ b/GFramework.Game.Abstractions/UI/IUiPageEnterParam.cs @@ -1,7 +1,9 @@ -namespace GFramework.Game.Abstractions.UI; +using GFramework.Game.Abstractions.Routing; + +namespace GFramework.Game.Abstractions.UI; /// /// UI页面进入参数接口 /// 该接口用于定义UI页面跳转时传递的参数数据结构 /// -public interface IUiPageEnterParam; \ No newline at end of file +public interface IUiPageEnterParam : IRouteContext; \ No newline at end of file diff --git a/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs b/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs index 8bfa0df..446092a 100644 --- a/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs +++ b/GFramework.Game.Abstractions/UI/IUiRouteGuard.cs @@ -1,22 +1,13 @@ +using GFramework.Game.Abstractions.Routing; + namespace GFramework.Game.Abstractions.UI; /// /// UI路由守卫接口 /// 用于拦截和处理UI路由切换,实现业务逻辑解耦 /// -public interface IUiRouteGuard +public interface IUiRouteGuard : IRouteGuard { - /// - /// 守卫优先级,数值越小越先执行 - /// - int Priority { get; } - - /// - /// 是否可中断后续守卫 - /// 如果返回 true,当该守卫返回 false 时,将停止执行后续守卫 - /// - bool CanInterrupt { get; } - /// /// 进入UI前的检查 /// @@ -30,5 +21,5 @@ public interface IUiRouteGuard /// /// 当前UI标识符 /// true表示允许离开,false表示拦截 - Task CanLeaveAsync(string uiKey); + new Task CanLeaveAsync(string uiKey); } \ No newline at end of file diff --git a/GFramework.Game.Abstractions/UI/IUiRouter.cs b/GFramework.Game.Abstractions/UI/IUiRouter.cs index c5ee283..b6b1887 100644 --- a/GFramework.Game.Abstractions/UI/IUiRouter.cs +++ b/GFramework.Game.Abstractions/UI/IUiRouter.cs @@ -113,28 +113,6 @@ public interface IUiRouter : ISystem /// bool Contains(string uiKey); - #region 路由守卫 - - /// - /// 注册路由守卫 - /// - /// 守卫实例 - void AddGuard(IUiRouteGuard guard); - - /// - /// 注册路由守卫(泛型方法) - /// - /// 守卫类型,必须实现 IUiRouteGuard 且有无参构造函数 - void AddGuard() where T : IUiRouteGuard, new(); - - /// - /// 移除路由守卫 - /// - /// 守卫实例 - void RemoveGuard(IUiRouteGuard guard); - - #endregion - #region Layer UI /// diff --git a/GFramework.Game.Tests/GFramework.Game.Tests.csproj b/GFramework.Game.Tests/GFramework.Game.Tests.csproj new file mode 100644 index 0000000..56fa021 --- /dev/null +++ b/GFramework.Game.Tests/GFramework.Game.Tests.csproj @@ -0,0 +1,23 @@ + + + + net8.0;net10.0 + disable + enable + false + true + + + + + + + + + + + + + + + diff --git a/GFramework.Game.Tests/GlobalUsings.cs b/GFramework.Game.Tests/GlobalUsings.cs new file mode 100644 index 0000000..6b9d4d6 --- /dev/null +++ b/GFramework.Game.Tests/GlobalUsings.cs @@ -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; \ No newline at end of file diff --git a/GFramework.Game.Tests/Routing/RouterBaseTests.cs b/GFramework.Game.Tests/Routing/RouterBaseTests.cs new file mode 100644 index 0000000..54992a1 --- /dev/null +++ b/GFramework.Game.Tests/Routing/RouterBaseTests.cs @@ -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; + +/// +/// RouterBase 单元测试 +/// +[TestFixture] +public class RouterBaseTests +{ + /// + /// 测试用路由项 + /// + private class TestRoute : IRoute + { + public string Key { get; set; } = string.Empty; + } + + /// + /// 测试用路由上下文 + /// + private class TestContext : IRouteContext + { + public string? Data { get; set; } + } + + /// + /// 测试用路由守卫 + /// + private class TestGuard : IRouteGuard + { + public Func>? EnterFunc { get; set; } + public Func>? LeaveFunc { get; set; } + public int Priority { get; set; } + public bool CanInterrupt { get; set; } + + public ValueTask CanEnterAsync(string routeKey, IRouteContext? context) + { + return EnterFunc?.Invoke(routeKey, context) ?? ValueTask.FromResult(true); + } + + public ValueTask CanLeaveAsync(string routeKey) + { + return LeaveFunc?.Invoke(routeKey) ?? ValueTask.FromResult(true); + } + } + + /// + /// 测试用路由器实现 + /// + private class TestRouter : RouterBase + { + public bool HandlersRegistered { get; private set; } + + // 暴露 Stack 用于测试 + public new Stack Stack => base.Stack; + + protected override void OnInit() + { + // 测试用路由器不需要初始化逻辑 + } + + protected override void RegisterHandlers() + { + HandlersRegistered = true; + } + + // 暴露 protected 方法用于测试 + public new Task ExecuteEnterGuardsAsync(string routeKey, TestContext? context) + { + return base.ExecuteEnterGuardsAsync(routeKey, context); + } + + public new Task 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(); + 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()); + } + + [Test] + public void AddGuard_WithNull_ShouldThrowArgumentNullException() + { + // Arrange + var router = new TestRouter(); + + // Act & Assert + Assert.Throws(() => 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(() => 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)); + } +} \ No newline at end of file diff --git a/GFramework.Game/Routing/RouterBase.cs b/GFramework.Game/Routing/RouterBase.cs new file mode 100644 index 0000000..1f73150 --- /dev/null +++ b/GFramework.Game/Routing/RouterBase.cs @@ -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; + +/// +/// 路由器基类,提供通用的路由管理功能 +/// +/// 路由项类型,必须实现 IRoute 接口 +/// 路由上下文类型,必须实现 IRouteContext 接口 +/// +/// 此基类提供了以下通用功能: +/// - 路由守卫管理 (AddGuard/RemoveGuard) +/// - 守卫执行逻辑 (ExecuteEnterGuardsAsync/ExecuteLeaveGuardsAsync) +/// - 路由栈管理 (Stack/Current/CurrentKey) +/// - 栈操作方法 (Contains/PeekKey/IsTop) +/// +public abstract class RouterBase : AbstractSystem + where TRoute : IRoute + where TContext : IRouteContext +{ + private static readonly ILogger Log = + LoggerFactoryResolver.Provider.CreateLogger(nameof(RouterBase)); + + /// + /// 路由守卫列表,按优先级排序 + /// + private readonly List> _guards = new(); + + /// + /// 路由栈,用于管理路由的显示顺序和导航历史 + /// + protected readonly Stack Stack = new(); + + /// + /// 获取当前路由 (栈顶元素) + /// + public TRoute? Current => Stack.Count > 0 ? Stack.Peek() : default; + + /// + /// 获取当前路由的键值 + /// + public string? CurrentKey => Current?.Key; + + /// + /// 获取栈深度 + /// + public int Count => Stack.Count; + + #region Abstract Methods + + /// + /// 注册过渡处理器 (由子类实现) + /// + /// + /// 子类应该在此方法中注册所有需要的过渡处理器。 + /// 此方法在 OnInit 中被调用。 + /// + protected abstract void RegisterHandlers(); + + #endregion + + #region Guard Management + + /// + /// 添加路由守卫 + /// + /// 路由守卫实例 + /// 当守卫实例为 null 时抛出 + public void AddGuard(IRouteGuard 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); + } + + /// + /// 添加路由守卫 (泛型版本) + /// + /// 守卫类型,必须实现 IRouteGuard 接口且有无参构造函数 + public void AddGuard() where T : IRouteGuard, new() + { + AddGuard(new T()); + } + + /// + /// 移除路由守卫 + /// + /// 要移除的路由守卫实例 + /// 当守卫实例为 null 时抛出 + public void RemoveGuard(IRouteGuard guard) + { + ArgumentNullException.ThrowIfNull(guard); + if (_guards.Remove(guard)) + Log.Debug("Guard removed: {0}", guard.GetType().Name); + } + + #endregion + + #region Guard Execution + + /// + /// 执行进入守卫检查 + /// + /// 路由键值 + /// 路由上下文 + /// 如果所有守卫都允许进入返回 true,否则返回 false + /// + /// 守卫按优先级从小到大依次执行。 + /// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。 + /// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。 + /// + protected async Task 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; + } + + /// + /// 执行离开守卫检查 + /// + /// 路由键值 + /// 如果所有守卫都允许离开返回 true,否则返回 false + /// + /// 守卫按优先级从小到大依次执行。 + /// 如果某个守卫返回 false 且 CanInterrupt 为 true,则中断后续守卫的执行。 + /// 如果某个守卫抛出异常且 CanInterrupt 为 true,则中断后续守卫的执行。 + /// + protected async Task 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 + + /// + /// 检查栈中是否包含指定路由 + /// + /// 路由键值 + /// 如果栈中包含指定路由返回 true,否则返回 false + public bool Contains(string routeKey) + { + return Stack.Any(r => r.Key == routeKey); + } + + /// + /// 获取栈顶路由的键值 + /// + /// 栈顶路由的键值,如果栈为空则返回空字符串 + public string PeekKey() + { + return Stack.Count == 0 ? string.Empty : Stack.Peek().Key; + } + + /// + /// 判断栈顶是否为指定路由 + /// + /// 路由键值 + /// 如果栈顶是指定路由返回 true,否则返回 false + public bool IsTop(string routeKey) + { + return Stack.Count != 0 && Stack.Peek().Key.Equals(routeKey); + } + + #endregion +} \ No newline at end of file diff --git a/GFramework.Game/Scene/SceneRouterBase.cs b/GFramework.Game/Scene/SceneRouterBase.cs index 2b3ccec..812644c 100644 --- a/GFramework.Game/Scene/SceneRouterBase.cs +++ b/GFramework.Game/Scene/SceneRouterBase.cs @@ -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; /// 实现了 接口,用于管理场景的加载、替换和卸载操作。 /// public abstract class SceneRouterBase - : AbstractSystem, ISceneRouter + : RouterBase, ISceneRouter { private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(SceneRouterBase)); - private readonly List _guards = new(); private readonly SceneTransitionPipeline _pipeline = new(); - private readonly Stack _stack = new(); private readonly SemaphoreSlim _transitionLock = new(1, 1); private ISceneFactory _factory = null!; @@ -45,17 +43,17 @@ public abstract class SceneRouterBase /// /// 获取当前场景行为对象。 /// - public ISceneBehavior? Current => _stack.Count > 0 ? _stack.Peek() : null; + public new ISceneBehavior? Current => Stack.Count > 0 ? Stack.Peek() : null; /// /// 获取当前场景的键名。 /// - public string? CurrentKey => Current?.Key; + public new string? CurrentKey => Current?.Key; /// /// 获取场景栈的只读视图,按压入顺序排列(从栈底到栈顶)。 /// - public IEnumerable Stack => _stack.Reverse(); + IEnumerable ISceneRouter.Stack => base.Stack.Reverse(); /// /// 获取是否正在进行场景转换。 @@ -115,9 +113,9 @@ public abstract class SceneRouterBase /// /// 场景键名。 /// 如果场景在栈中返回true,否则返回false。 - 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); } - /// - /// 添加场景路由守卫。 - /// - /// 守卫实例。 - 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); - } - } - - /// - /// 添加场景路由守卫(泛型版本)。 - /// - /// 守卫类型。 - public void AddGuard() where T : ISceneRouteGuard, new() - { - AddGuard(new T()); - } - - /// - /// 移除场景路由守卫。 - /// - /// 守卫实例。 - public void RemoveGuard(ISceneRouteGuard guard) - { - if (_guards.Remove(guard)) - { - Log.Debug("Guard removed: {0}", guard.GetType().Name); - } - } - /// /// 注册场景过渡处理器的抽象方法,由子类实现。 /// - protected abstract void RegisterHandlers(); + protected override abstract void RegisterHandlers(); /// /// 系统初始化方法,获取场景工厂并注册处理器。 @@ -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 /// 异步任务。 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 /// 异步任务。 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); } - /// - /// 执行进入场景的守卫检查。 - /// 按优先级顺序执行所有守卫的CanEnterAsync方法。 - /// - /// 场景键名。 - /// 进入参数。 - /// 如果所有守卫都允许进入返回true,否则返回false。 - private async Task 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; - } - - /// - /// 执行离开场景的守卫检查。 - /// 按优先级顺序执行所有守卫的CanLeaveAsync方法。 - /// - /// 场景键名。 - /// 如果所有守卫都允许离开返回true,否则返回false。 - private async Task 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 } \ No newline at end of file diff --git a/GFramework.Game/UI/UiRouterBase.cs b/GFramework.Game/UI/UiRouterBase.cs index 529c2cb..f105cbb 100644 --- a/GFramework.Game/UI/UiRouterBase.cs +++ b/GFramework.Game/UI/UiRouterBase.cs @@ -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页面的导航、显示、隐藏以及生命周期管理 /// -public abstract class UiRouterBase : AbstractSystem, IUiRouter +public abstract class UiRouterBase : RouterBase, IUiRouter { private static readonly ILogger Log = LoggerFactoryResolver.Provider.CreateLogger(nameof(UiRouterBase)); - /// - /// 路由守卫列表,用于控制UI页面的进入和离开 - /// - private readonly List _guards = new(); - /// /// 层级管理字典(非栈层级),用于管理Overlay、Modal、Toast等浮层UI /// Key: UiLayer枚举值, Value: InstanceId到PageBehavior的映射字典 @@ -31,11 +26,6 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// private readonly UiTransitionPipeline _pipeline = new(); - /// - /// 页面栈,用于管理UI页面的显示顺序和导航历史 - /// - private readonly Stack _stack = new(); - /// /// UI工厂实例,用于创建UI页面和相关对象 /// @@ -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 /// 页面弹出策略 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 /// 获取栈顶元素的键值 /// /// 栈顶UI页面的键值,如果栈为空则返回空字符串 - 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; } /// @@ -251,7 +241,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 栈顶UI页面行为实例,如果栈为空则返回null public IUiPageBehavior? Peek() { - return _stack.Count == 0 ? null : _stack.Peek(); + return Stack.Count == 0 ? null : Stack.Peek(); } /// @@ -259,9 +249,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// /// 要检查的UI页面键值 /// 如果栈顶是指定UI则返回true,否则返回false - 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); } /// @@ -269,15 +259,15 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// /// 要检查的UI页面键值 /// 如果栈中包含指定UI则返回true,否则返回false - 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)); } /// /// 获取栈深度 /// - 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 - - /// - /// 注册路由守卫 - /// - /// 路由守卫实例 - /// 当守卫实例为null时抛出 - 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); - } - - /// - /// 注册路由守卫(泛型) - /// - /// 路由守卫类型,必须实现IUiRouteGuard接口且有无参构造函数 - public void AddGuard() where T : IUiRouteGuard, new() - { - AddGuard(new T()); - } - - /// - /// 移除路由守卫 - /// - /// 要移除的路由守卫实例 - /// 当守卫实例为null时抛出 - public void RemoveGuard(IUiRouteGuard guard) - { - ArgumentNullException.ThrowIfNull(guard); - if (_guards.Remove(guard)) - Log.Debug("Guard removed: {0}", guard.GetType().Name); - } - - #endregion - #region Initialization /// @@ -525,7 +470,7 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 抽象方法,用于注册具体的处理程序。 /// 子类必须实现此方法以完成特定的处理逻辑注册。 /// - protected abstract void RegisterHandlers(); + protected override abstract void RegisterHandlers(); #endregion @@ -655,9 +600,9 @@ public abstract class UiRouterBase : AbstractSystem, IUiRouter /// 过渡策略 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 /// 页面弹出策略 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 /// 页面弹出策略 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); } - /// - /// 执行进入守卫检查 - /// - /// UI页面键值 - /// 页面进入参数 - /// 如果允许进入则返回true,否则返回false - private async Task 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; - } - - /// - /// 执行离开守卫检查 - /// - /// UI页面键值 - /// 如果允许离开则返回true,否则返回false - private async Task 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 } \ No newline at end of file diff --git a/GFramework.csproj b/GFramework.csproj index aa36eaf..20f2339 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -55,6 +55,7 @@ + @@ -93,6 +94,7 @@ + @@ -117,6 +119,7 @@ + diff --git a/GFramework.sln b/GFramework.sln index 04b1d8f..7076c6a 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -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