From ad5a7b68a3994b15934f995572eb549694dbb6a4 Mon Sep 17 00:00:00 2001 From: cneicy Date: Wed, 26 Aug 2026 01:15:48 +0800 Subject: [PATCH] feat(sdk): migrate to EventBus 2.0 Replace the legacy EventBase runtime with generated multi-bus bindings and explicit scheduling. Migrate app, data, network, demo, and mod consumers; add generated network-event registration and owner-scoped mod content overrides. --- .../UnityAdapter/ExampleGreedUnityMod.cs | 6 +- .../Runtime/ReplacedLanRuntime.cs | 2 +- .../Runtime/ReplacedPersonEvents.cs | 14 +- .../Runtime/ReplacedPersonGameService.cs | 22 +- .../ShrinkApp.Core/Runtime/ShrinkAppHost.cs | 4 +- .../ShrinkApp.Core/Runtime/ShrinkAppTypes.cs | 4 +- Assets/Modules/ShrinkApp.Core/package.json | 2 +- .../ShrinkCommandEventBusBridge.cs | 28 +- .../ShrinkCommandEventBusBridgeEvents.cs | 13 +- .../package.json | 2 +- .../Runtime/ShrinkAppLoaderHost.cs | 2 +- .../Tests/ShrinkAppLoaderHostTests.cs | 17 +- .../Runtime/ShrinkEventBusEffects.cs | 35 +- .../Tests/EventBusEffectTests.cs | 47 +- .../package.json | 4 +- .../DataSaverEventBusBridge.cs | 18 +- .../DataSaverEvents.cs | 20 +- .../README.md | 158 +- .../package.json | 2 +- Assets/Modules/ShrinkDataSaver/README.md | 14 +- Assets/Modules/ShrinkEventBus/CHANGELOG.md | 20 + .../CodeGen/Editor/EventBusILPostProcessor.cs | 624 ++++--- .../Editor/PostProcessorAssemblyResolver.cs | 2 +- .../Editor/PostProcessorReflectionImporter.cs | 4 +- .../Editor/EventBusViewerWindow.cs | 1509 +++++++++-------- Assets/Modules/ShrinkEventBus/README.md | 692 ++------ .../ShrinkEventBus/Runtime/AssemblyInfo.cs | 2 + .../ShrinkEventBus/Runtime/Attributes.cs | 42 - .../ShrinkEventBus/Runtime/Attributes.cs.meta | 3 - .../Runtime/EventAutoRegHelper.cs | 36 - .../Runtime/EventAutoRegHelper.cs.meta | 3 - .../ShrinkEventBus/Runtime/EventBase.cs | 146 -- .../ShrinkEventBus/Runtime/EventBase.cs.meta | 3 - .../ShrinkEventBus/Runtime/EventBus.cs | 259 +-- .../Runtime/EventBusBenchmark.cs | 519 ------ .../Runtime/EventBusBenchmark.cs.meta | 3 - .../Runtime/EventBusGeneratedRegistry.cs | 53 - .../Runtime/EventBusRegHelper.cs | 193 --- .../Runtime/EventBusRegHelper.cs.meta | 2 - .../Runtime/EventCloneUtility.cs | 75 - .../Runtime/EventHandlerInfo.cs | 141 -- .../Runtime/EventHandlerInfo.cs.meta | 3 - .../ShrinkEventBus/Runtime/EventPool.cs | 47 - .../ShrinkEventBus/Runtime/EventPool.cs.meta | 3 - .../ShrinkEventBus/Runtime/EventPriority.cs | 27 - .../Runtime/EventPriority.cs.meta | 3 - .../ShrinkEventBus/Runtime/Exceptions.cs | 20 - .../ShrinkEventBus/Runtime/Exceptions.cs.meta | 3 - .../ShrinkEventBus/Runtime/IShrinkEventBus.cs | 110 +- .../ShrinkEventBus/Runtime/ListenerList.cs | 262 --- .../Runtime/ListenerList.cs.meta | 3 - .../Runtime/ShrinkBusSchedulers.cs | 546 ++++++ ...ry.cs.meta => ShrinkBusSchedulers.cs.meta} | 2 +- .../Runtime/ShrinkEventAttributes.cs | 47 + ....cs.meta => ShrinkEventAttributes.cs.meta} | 2 +- .../Runtime/ShrinkEventBindings.cs | 293 ++++ .../ShrinkEventBindings.cs.meta} | 2 +- .../Runtime/ShrinkEventBusBuilder.cs | 70 +- .../Runtime/ShrinkEventBusCore.cs | 272 +++ .../ShrinkEventBusCore.cs.meta} | 2 +- .../Runtime/ShrinkEventBusInstance.cs | 962 ++++------- .../Runtime/ShrinkEventContracts.cs | 213 +++ .../Runtime/ShrinkEventContracts.cs.meta | 11 + .../Runtime/ShrinkMonoEventScope.cs | 71 + .../Runtime/ShrinkMonoEventScope.cs.meta | 11 + .../ShrinkEventBus.PlayMode.Tests.asmdef | 15 + .../ShrinkEventBus.PlayMode.Tests.asmdef.meta | 7 + .../ShrinkMonoEventScopePlayModeTests.cs | 48 + .../ShrinkMonoEventScopePlayModeTests.cs.meta | 11 + .../Tests/EventBusDispatchTests.cs | 200 --- .../Tests/EventBusPriorityTests.cs | 120 -- .../Tests/EventBusRegistrationTests.cs | 228 --- .../Tests/EventBusRegistrationTests.cs.meta | 11 - .../Tests/EventBusSubscriptionTests.cs | 94 - .../Tests/EventBusSubscriptionTests.cs.meta | 11 - .../Tests/ShrinkEventBusV2Tests.cs | 440 +++++ .../Tests/ShrinkEventBusV2Tests.cs.meta | 11 + Assets/Modules/ShrinkEventBus/package.json | 4 +- .../Modules/ShrinkModFramework/CHANGELOG.md | 5 +- .../ExternalModProjectTemplate/README.md.txt | 6 +- .../__PROJECT_NAME__.csproj.txt | 3 + .../src/__PROJECT_NAME__Contracts.cs.txt | 2 +- .../src/__PROJECT_NAME__Mod.cs.txt | 12 +- .../Runtime/__PROJECT_NAME__Contracts.cs.txt | 2 +- .../Runtime/__PROJECT_NAME__Mod.cs.txt | 12 +- .../Scaffolding/ShrinkModSdkExporter.cs | 49 +- Assets/Modules/ShrinkModFramework/README.md | 59 +- .../Runtime/Context/ShrinkModComponent.cs | 11 + .../Runtime/Context/ShrinkModContextHost.cs | 2 +- .../Runtime/Context/ShrinkModCordisRuntime.cs | 2 +- .../Runtime/Core/ShrinkModContext.cs | 15 +- .../Runtime/Core/ShrinkModHandle.cs | 3 + .../ShrinkModOptionalRuntimeIntegration.cs | 31 +- .../Runtime/Loading/ShrinkModLoader.cs | 21 +- .../Runtime/Registry/ShrinkModRegistry.cs | 167 +- .../Registry/ShrinkModRegistryManager.cs | 7 +- .../Runtime/ShrinkModFramework.Runtime.asmdef | 1 + .../Fixtures/ExternalFixture.Mod.V1.dll.bytes | Bin 4096 -> 5120 bytes .../ExternalFixture.Mod.V2.Failing.dll.bytes | Bin 4096 -> 5120 bytes .../Fixtures/ExternalFixture.Mod.V3.dll.bytes | Bin 4096 -> 5120 bytes .../Tests/ShrinkModContextHostTests.cs | 109 +- .../Modules/ShrinkModFramework/package.json | 5 +- .../CodeGen.meta | 8 + .../CodeGen/Editor.meta | 8 + .../Editor/NetworkEventILPostProcessor.cs | 184 ++ .../NetworkEventILPostProcessor.cs.meta | 11 + ...nity.ShrinkNetwork.EventBus.CodeGen.asmdef | 19 + ...ShrinkNetwork.EventBus.CodeGen.asmdef.meta | 7 + .../README.md | 238 +-- .../ShrinkNetworkEventBusBridge.cs | 177 +- .../ShrinkNetworkEventBusBridgeExtensions.cs | 6 +- .../ShrinkNetworkEventBusBridgeOptions.cs | 2 +- .../ShrinkNetworkEventRegistry.cs | 56 + .../ShrinkNetworkEventRegistry.cs.meta | 11 + .../Tests.meta | 8 + ...kNetwork.Integration.EventBus.Tests.asmdef | 18 + ...ork.Integration.EventBus.Tests.asmdef.meta | 7 + .../Tests/ShrinkNetworkEventCodeGenTests.cs | 36 + .../ShrinkNetworkEventCodeGenTests.cs.meta | 11 + .../package.json | 4 +- .../ShrinkNetworkSemanticScanner.cs | 12 +- Assets/Modules/ShrinkNetwork/README.md | 2 +- .../Editor/ShrinkRegistryILPostProcessor.cs | 209 --- ...kEmbeddedHostNetworkDemoSceneController.cs | 29 +- .../ExternalFixture.Mod.V1.cs | 25 + .../ExternalFixture.Mod.V2.cs | 27 + .../ExternalFixture.Mod.V3.cs | 25 + .../Rebuild-ShrinkModFixtures.ps1 | 43 + .../ShrinkModFixtureBuilder.csproj | 18 + 129 files changed, 5071 insertions(+), 5584 deletions(-) delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/Attributes.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/Attributes.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventAutoRegHelper.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventAutoRegHelper.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBase.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBase.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBusBenchmark.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBusBenchmark.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBusGeneratedRegistry.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBusRegHelper.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventBusRegHelper.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventCloneUtility.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventHandlerInfo.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventHandlerInfo.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventPool.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventPool.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventPriority.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/EventPriority.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/Exceptions.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/Exceptions.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ListenerList.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ListenerList.cs.meta create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkBusSchedulers.cs rename Assets/Modules/ShrinkEventBus/Runtime/{EventBusGeneratedRegistry.cs.meta => ShrinkBusSchedulers.cs.meta} (83%) create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkEventAttributes.cs rename Assets/Modules/ShrinkEventBus/Runtime/{EventCloneUtility.cs.meta => ShrinkEventAttributes.cs.meta} (83%) create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkEventBindings.cs rename Assets/Modules/ShrinkEventBus/{Tests/EventBusDispatchTests.cs.meta => Runtime/ShrinkEventBindings.cs.meta} (83%) create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkEventBusCore.cs rename Assets/Modules/ShrinkEventBus/{Tests/EventBusPriorityTests.cs.meta => Runtime/ShrinkEventBusCore.cs.meta} (83%) create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkEventContracts.cs create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkEventContracts.cs.meta create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkMonoEventScope.cs create mode 100644 Assets/Modules/ShrinkEventBus/Runtime/ShrinkMonoEventScope.cs.meta create mode 100644 Assets/Modules/ShrinkEventBus/Tests.PlayMode/ShrinkEventBus.PlayMode.Tests.asmdef create mode 100644 Assets/Modules/ShrinkEventBus/Tests.PlayMode/ShrinkEventBus.PlayMode.Tests.asmdef.meta create mode 100644 Assets/Modules/ShrinkEventBus/Tests.PlayMode/ShrinkMonoEventScopePlayModeTests.cs create mode 100644 Assets/Modules/ShrinkEventBus/Tests.PlayMode/ShrinkMonoEventScopePlayModeTests.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Tests/EventBusDispatchTests.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Tests/EventBusPriorityTests.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Tests/EventBusRegistrationTests.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Tests/EventBusRegistrationTests.cs.meta delete mode 100644 Assets/Modules/ShrinkEventBus/Tests/EventBusSubscriptionTests.cs delete mode 100644 Assets/Modules/ShrinkEventBus/Tests/EventBusSubscriptionTests.cs.meta create mode 100644 Assets/Modules/ShrinkEventBus/Tests/ShrinkEventBusV2Tests.cs create mode 100644 Assets/Modules/ShrinkEventBus/Tests/ShrinkEventBusV2Tests.cs.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/CodeGen.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/CodeGen/Editor.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/CodeGen/Editor/NetworkEventILPostProcessor.cs create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/CodeGen/Editor/NetworkEventILPostProcessor.cs.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/CodeGen/Editor/Unity.ShrinkNetwork.EventBus.CodeGen.asmdef.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/ShrinkNetworkEventRegistry.cs create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/ShrinkNetworkEventRegistry.cs.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/Tests.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/Tests/ShrinkNetwork.Integration.EventBus.Tests.asmdef.meta create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/Tests/ShrinkNetworkEventCodeGenTests.cs create mode 100644 Assets/Modules/ShrinkNetwork.Integration.EventBus/Tests/ShrinkNetworkEventCodeGenTests.cs.meta create mode 100644 Tools/ShrinkModFixtureBuilder/ExternalFixture.Mod.V1.cs create mode 100644 Tools/ShrinkModFixtureBuilder/ExternalFixture.Mod.V2.cs create mode 100644 Tools/ShrinkModFixtureBuilder/ExternalFixture.Mod.V3.cs create mode 100644 Tools/ShrinkModFixtureBuilder/Rebuild-ShrinkModFixtures.ps1 create mode 100644 Tools/ShrinkModFixtureBuilder/ShrinkModFixtureBuilder.csproj diff --git a/Assets/Demos/ReplacedPerson/Mods/ExampleGreedMod/UnityAdapter/ExampleGreedUnityMod.cs b/Assets/Demos/ReplacedPerson/Mods/ExampleGreedMod/UnityAdapter/ExampleGreedUnityMod.cs index 5e801fd..1b10361 100644 --- a/Assets/Demos/ReplacedPerson/Mods/ExampleGreedMod/UnityAdapter/ExampleGreedUnityMod.cs +++ b/Assets/Demos/ReplacedPerson/Mods/ExampleGreedMod/UnityAdapter/ExampleGreedUnityMod.cs @@ -11,8 +11,10 @@ namespace ExampleGreedMod { public override void OnRegisterContent(ShrinkModContext context) { - context.GetRegistry(ReplacedPersonModBridge.RegistryName) - .Register(context.ModInfo.ModId, context.ModInfo.ModId, new ExampleGreedRules()); + context.RegisterContent( + ReplacedPersonModBridge.RegistryName, + "rules", + new ExampleGreedRules()); } public override void OnReady(ShrinkModContext context) diff --git a/Assets/Demos/ReplacedPerson/Runtime/ReplacedLanRuntime.cs b/Assets/Demos/ReplacedPerson/Runtime/ReplacedLanRuntime.cs index 9e80908..488917d 100644 --- a/Assets/Demos/ReplacedPerson/Runtime/ReplacedLanRuntime.cs +++ b/Assets/Demos/ReplacedPerson/Runtime/ReplacedLanRuntime.cs @@ -477,6 +477,6 @@ namespace ReplacedPerson.Runtime private static ReplacedCommandResult Reject(string code, string detail) => new() { ErrorCode = code, Message = detail }; private static void Publish(string status, string detail) => - EventBus.TriggerEvent(new ReplacedNetworkStatusEvent { Status = status, Detail = detail }); + EventBus.Post(new ReplacedNetworkStatusEvent { Status = status, Detail = detail }); } } diff --git a/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonEvents.cs b/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonEvents.cs index 5400531..9c7b2e0 100644 --- a/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonEvents.cs +++ b/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonEvents.cs @@ -5,7 +5,7 @@ using ShrinkEventBus; namespace ReplacedPerson.Runtime { - public sealed class ReplacedMatchPhaseEvent : EventBase + public sealed class ReplacedMatchPhaseEvent : IShrinkEvent { public int Round { get; set; } public ReplacedMatchPhase MatchPhase { get; set; } @@ -13,39 +13,39 @@ namespace ReplacedPerson.Runtime public string StateHash { get; set; } = string.Empty; } - public sealed class ReplacedCardsCommittedEvent : EventBase + public sealed class ReplacedCardsCommittedEvent : IShrinkEvent { public int PlayerIndex { get; set; } public string[] NormalCardIds { get; set; } = System.Array.Empty(); public string[] EndCardIds { get; set; } = System.Array.Empty(); } - public sealed class ReplacedDiceEvent : EventBase + public sealed class ReplacedDiceEvent : IShrinkEvent { public int Slot { get; set; } public int PlayerOneRoll { get; set; } public int PlayerTwoRoll { get; set; } } - public sealed class ReplacedDamageEvent : EventBase + public sealed class ReplacedDamageEvent : IShrinkEvent { public int TargetPlayer { get; set; } public int Amount { get; set; } public int RemainingHealth { get; set; } } - public sealed class ReplacedRewardEvent : EventBase + public sealed class ReplacedRewardEvent : IShrinkEvent { public string RewardId { get; set; } = string.Empty; } - public sealed class ReplacedSaveEvent : EventBase + public sealed class ReplacedSaveEvent : IShrinkEvent { public string Operation { get; set; } = string.Empty; public bool Success { get; set; } } - public sealed class ReplacedNetworkStatusEvent : EventBase + public sealed class ReplacedNetworkStatusEvent : IShrinkEvent { public string Status { get; set; } = string.Empty; public string Detail { get; set; } = string.Empty; diff --git a/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonGameService.cs b/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonGameService.cs index b22053d..bd22301 100644 --- a/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonGameService.cs +++ b/Assets/Demos/ReplacedPerson/Runtime/ReplacedPersonGameService.cs @@ -69,7 +69,7 @@ namespace ReplacedPerson.Runtime var result = Match.Apply(command); if (result.Accepted && !result.Duplicate) { - EventBus.TriggerEvent(new ReplacedCardsCommittedEvent + EventBus.Post(new ReplacedCardsCommittedEvent { PlayerIndex = 0, NormalCardIds = submission.NormalCardIds.ToArray(), @@ -87,7 +87,7 @@ namespace ReplacedPerson.Runtime { if (!Content.Current.Cards.ContainsKey(cardId)) throw new ArgumentException("Unknown card: " + cardId); _saveData.CollectedCards.Add(cardId); - EventBus.TriggerEvent(new ReplacedRewardEvent { RewardId = cardId }); + EventBus.Post(new ReplacedRewardEvent { RewardId = cardId }); Changed?.Invoke(); } @@ -106,11 +106,11 @@ namespace ReplacedPerson.Runtime try { await ShrinkSave.SaveSlotAsync(slot, new SaveOptions { SlotName = "被替代之人" }); - EventBus.TriggerEvent(new ReplacedSaveEvent { Operation = "save", Success = true }); + EventBus.Post(new ReplacedSaveEvent { Operation = "save", Success = true }); } catch { - EventBus.TriggerEvent(new ReplacedSaveEvent { Operation = "save", Success = false }); + EventBus.Post(new ReplacedSaveEvent { Operation = "save", Success = false }); throw; } } @@ -120,12 +120,12 @@ namespace ReplacedPerson.Runtime try { await ShrinkSave.LoadSlotAsync(slot); - EventBus.TriggerEvent(new ReplacedSaveEvent { Operation = "load", Success = true }); + EventBus.Post(new ReplacedSaveEvent { Operation = "load", Success = true }); Changed?.Invoke(); } catch { - EventBus.TriggerEvent(new ReplacedSaveEvent { Operation = "load", Success = false }); + EventBus.Post(new ReplacedSaveEvent { Operation = "load", Success = false }); throw; } } @@ -171,7 +171,7 @@ namespace ReplacedPerson.Runtime var before = Match.State.Clone(); var result = Match.Apply(command); if (!result.Accepted) throw new InvalidOperationException("AI produced illegal command: " + result.ErrorCode); - EventBus.TriggerEvent(new ReplacedCardsCommittedEvent + EventBus.Post(new ReplacedCardsCommittedEvent { PlayerIndex = 1, NormalCardIds = submission.NormalCardIds.ToArray(), @@ -194,7 +194,7 @@ namespace ReplacedPerson.Runtime { if (Match == null) return; var state = after ?? Match.State; - EventBus.TriggerEvent(new ReplacedMatchPhaseEvent + EventBus.Post(new ReplacedMatchPhaseEvent { Round = state.Round, MatchPhase = state.Phase, @@ -204,7 +204,7 @@ namespace ReplacedPerson.Runtime if (before == null || !SameResolution(before.LastResolution, state.LastResolution)) foreach (var slot in state.LastResolution) { - EventBus.TriggerEvent(new ReplacedDiceEvent + EventBus.Post(new ReplacedDiceEvent { Slot = slot.SlotIndex, PlayerOneRoll = slot.PlayerOneRoll, @@ -216,7 +216,7 @@ namespace ReplacedPerson.Runtime { var damage = Math.Max(0, before.Players[i].Health - state.Players[i].Health); if (damage > 0) - EventBus.TriggerEvent(new ReplacedDamageEvent { TargetPlayer = i, Amount = damage, RemainingHealth = state.Players[i].Health }); + EventBus.Post(new ReplacedDamageEvent { TargetPlayer = i, Amount = damage, RemainingHealth = state.Players[i].Health }); } } @@ -246,7 +246,7 @@ namespace ReplacedPerson.Runtime var target = reward.StartsWith("ornament.", StringComparison.Ordinal) ? _saveData.Ornaments : _saveData.CollectedCards; if (target.Contains(reward)) continue; target.Add(reward); - EventBus.TriggerEvent(new ReplacedRewardEvent { RewardId = reward }); + EventBus.Post(new ReplacedRewardEvent { RewardId = reward }); } _saveData.ChapterProgress = Math.Max(_saveData.ChapterProgress, ActiveEnemyId == "greed.full" ? 2 : 1); _saveData.RecentReplay = Match.Replay.Select(value => value.Clone()).ToList(); diff --git a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs index 5c01775..33adaae 100644 --- a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs +++ b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppHost.cs @@ -54,7 +54,7 @@ namespace ShrinkApp { HasFailed = true; LastError = ex.Message; - EventBus.TriggerEvent(new ShrinkAppStartFailedEvent + EventBus.Post(new ShrinkAppStartFailedEvent { ErrorMessage = ex.Message, FailedModuleId = _startedModules.LastOrDefault() ?? string.Empty @@ -75,7 +75,7 @@ namespace ShrinkApp return; _publishedStartedEvent = true; - EventBus.TriggerEvent(new ShrinkAppStartedEvent + EventBus.Post(new ShrinkAppStartedEvent { ModuleIds = _startedModules.ToArray() }); diff --git a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs index f2bf101..28159b1 100644 --- a/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs +++ b/Assets/Modules/ShrinkApp.Core/Runtime/ShrinkAppTypes.cs @@ -168,12 +168,12 @@ namespace ShrinkApp public ShrinkAppHostingMode hostingMode = ShrinkAppHostingMode.ClassicHost; } - public sealed class ShrinkAppStartedEvent : EventBase + public sealed class ShrinkAppStartedEvent : IShrinkEvent { public IReadOnlyList ModuleIds { get; set; } = Array.Empty(); } - public sealed class ShrinkAppStartFailedEvent : EventBase + public sealed class ShrinkAppStartFailedEvent : IShrinkEvent { public string ErrorMessage { get; set; } = string.Empty; public string FailedModuleId { get; set; } = string.Empty; diff --git a/Assets/Modules/ShrinkApp.Core/package.json b/Assets/Modules/ShrinkApp.Core/package.json index d020fed..e8ebe78 100644 --- a/Assets/Modules/ShrinkApp.Core/package.json +++ b/Assets/Modules/ShrinkApp.Core/package.json @@ -5,7 +5,7 @@ "description": "Shrink 系列统一宿主层,提供模块安装器、服务容器与统一启动流程。", "unity": "2022.3", "dependencies": { - "com.cneicy.shrink-eventbus": "1.3.0", + "com.cneicy.shrink-eventbus": "2.0.0", "com.cneicy.shrink-shared-codegen": "0.1.0", "com.cysharp.unitask": "2.5.10" }, diff --git a/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridge.cs b/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridge.cs index 1eea8cf..78d8889 100644 --- a/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridge.cs +++ b/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridge.cs @@ -8,6 +8,16 @@ namespace ShrinkCommand.Integration { public static class ShrinkCommandEventBusBridge { + [ShrinkEventSubscriber(OwnerId = "shrink.command", DefaultBus = "game")] + private sealed class CommandRequestSubscriber + { + [ShrinkSubscribe(Priority = ShrinkEventPriority.Lowest)] + private UniTask OnExecuteRequest(ShrinkCommandExecuteRequestEvent eventArgs) + { + return HandleExecuteRequestAsync(eventArgs); + } + } + private sealed class ServiceRegistration { public string ServiceName = ShrinkCommandConstants.DefaultServiceName; @@ -20,7 +30,7 @@ namespace ShrinkCommand.Integration private static readonly object SyncRoot = new(); private static readonly Dictionary RegisteredByName = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary RegisteredByService = new(); - private static bool _initialized; + private static IDisposable? _requestHandlerBinding; [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetStaticState() @@ -37,7 +47,8 @@ namespace ShrinkCommand.Integration RegisteredByService.Clear(); RegisteredByName.Clear(); - _initialized = false; + _requestHandlerBinding?.Dispose(); + _requestHandlerBinding = null; } } @@ -113,17 +124,16 @@ namespace ShrinkCommand.Integration Source = source }; - await EventBus.TriggerEventAsync(requestEvent); + await EventBus.PostAsync(requestEvent); return requestEvent.ExecutionResult ?? ShrinkCommandExecutionResult.Failure("没有命令桥处理该请求。"); } private static void EnsureInitialized() { - if (_initialized) + if (_requestHandlerBinding != null) return; - _initialized = true; - EventBus.RegisterEvent(HandleExecuteRequestAsync, EventPriority.LOWEST); + _requestHandlerBinding = EventBus.Attach(new CommandRequestSubscriber()); } private static async UniTask HandleExecuteRequestAsync(ShrinkCommandExecuteRequestEvent eventArgs) @@ -159,7 +169,7 @@ namespace ShrinkCommand.Integration private static void PublishExecuting(ServiceRegistration registration, ShrinkCommandExecutingInfo info) { - EventBus.TriggerEvent(new ShrinkCommandExecutingEvent + EventBus.Post(new ShrinkCommandExecutingEvent { ServiceName = registration.ServiceName, RawInput = info.RawInput, @@ -174,7 +184,7 @@ namespace ShrinkCommand.Integration { var commandPath = info.Command?.Path ?? string.Empty; var arguments = new Dictionary(info.Arguments, StringComparer.OrdinalIgnoreCase); - EventBus.TriggerEvent(new ShrinkCommandExecutedEvent + EventBus.Post(new ShrinkCommandExecutedEvent { ServiceName = registration.ServiceName, RawInput = info.RawInput, @@ -189,7 +199,7 @@ namespace ShrinkCommand.Integration if (info.Result.IsSuccess) return; - EventBus.TriggerEvent(new ShrinkCommandFailedEvent + EventBus.Post(new ShrinkCommandFailedEvent { ServiceName = registration.ServiceName, RawInput = info.RawInput, diff --git a/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridgeEvents.cs b/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridgeEvents.cs index 77269be..05f4621 100644 --- a/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridgeEvents.cs +++ b/Assets/Modules/ShrinkCommand.Integration.EventBus/ShrinkCommandEventBusBridgeEvents.cs @@ -4,17 +4,20 @@ using ShrinkEventBus; namespace ShrinkCommand.Integration { - [Cancelable] - public sealed class ShrinkCommandExecuteRequestEvent : EventBase + public sealed class ShrinkCommandExecuteRequestEvent : IShrinkCancelableEvent { + private bool _isCanceled; + public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; public string RawInput { get; set; } = string.Empty; public IShrinkCommandSource? Source { get; set; } public bool IsHandled { get; set; } public ShrinkCommandExecutionResult? ExecutionResult { get; set; } + public bool IsCanceled => _isCanceled; + public void SetCanceled(bool value) => _isCanceled = value; } - public sealed class ShrinkCommandExecutingEvent : EventBase + public sealed class ShrinkCommandExecutingEvent : IShrinkEvent { public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; public string RawInput { get; set; } = string.Empty; @@ -24,7 +27,7 @@ namespace ShrinkCommand.Integration public Dictionary Arguments { get; set; } = new(); } - public sealed class ShrinkCommandExecutedEvent : EventBase + public sealed class ShrinkCommandExecutedEvent : IShrinkEvent { public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; public string RawInput { get; set; } = string.Empty; @@ -36,7 +39,7 @@ namespace ShrinkCommand.Integration public string Message { get; set; } = string.Empty; } - public sealed class ShrinkCommandFailedEvent : EventBase + public sealed class ShrinkCommandFailedEvent : IShrinkEvent { public string ServiceName { get; set; } = ShrinkCommandConstants.DefaultServiceName; public string RawInput { get; set; } = string.Empty; diff --git a/Assets/Modules/ShrinkCommand.Integration.EventBus/package.json b/Assets/Modules/ShrinkCommand.Integration.EventBus/package.json index dbf9433..a1d5153 100644 --- a/Assets/Modules/ShrinkCommand.Integration.EventBus/package.json +++ b/Assets/Modules/ShrinkCommand.Integration.EventBus/package.json @@ -7,7 +7,7 @@ "dependencies": { "com.cneicy.shrink-command": "0.2.0", "com.cneicy.shrink-context-core": "0.1.0", - "com.cneicy.shrink-eventbus": "1.3.0" + "com.cneicy.shrink-eventbus": "2.0.0" }, "keywords": ["command", "eventbus", "integration", "bridge"], "author": { diff --git a/Assets/Modules/ShrinkContext.AppAdapter/Runtime/ShrinkAppLoaderHost.cs b/Assets/Modules/ShrinkContext.AppAdapter/Runtime/ShrinkAppLoaderHost.cs index 532a6de..9c5f5a2 100644 --- a/Assets/Modules/ShrinkContext.AppAdapter/Runtime/ShrinkAppLoaderHost.cs +++ b/Assets/Modules/ShrinkContext.AppAdapter/Runtime/ShrinkAppLoaderHost.cs @@ -214,7 +214,7 @@ namespace ShrinkContext.AppAdapter $"waiting={WaitingModuleIds.Count} disabled={_disabled.Count} " + $"modules=[{string.Join(", ", active)}]"); - EventBus.TriggerEvent(new ShrinkAppStartedEvent + EventBus.Post(new ShrinkAppStartedEvent { ModuleIds = active.ToArray() }); diff --git a/Assets/Modules/ShrinkContext.AppAdapter/Tests/ShrinkAppLoaderHostTests.cs b/Assets/Modules/ShrinkContext.AppAdapter/Tests/ShrinkAppLoaderHostTests.cs index 988c8ba..9a61a5f 100644 --- a/Assets/Modules/ShrinkContext.AppAdapter/Tests/ShrinkAppLoaderHostTests.cs +++ b/Assets/Modules/ShrinkContext.AppAdapter/Tests/ShrinkAppLoaderHostTests.cs @@ -19,6 +19,15 @@ namespace ShrinkContext.AppAdapter.Tests /// public class ShrinkAppLoaderHostTests { + [ShrinkEventSubscriber(DefaultBus = "game")] + private sealed class AppStartedProbe + { + public ShrinkAppStartedEvent? Value { get; private set; } + + [ShrinkSubscribe] + private void OnStarted(ShrinkAppStartedEvent value) => Value = value; + } + private ShrinkAppSettings _settings = null!; [SetUp] @@ -46,8 +55,8 @@ namespace ShrinkContext.AppAdapter.Tests new FakeInstaller("app.a", "a"), new FakeInstaller("app.b", "b") { DependsOn = new[] { "app.a" } }); - ShrinkAppStartedEvent? started = null; - using (var subscription = EventBus.SubscribeEvent(e => started = e)) + var probe = new AppStartedProbe(); + using (EventBus.Attach(probe)) { TestAwait.Run(host.StartAsync()); } @@ -55,8 +64,8 @@ namespace ShrinkContext.AppAdapter.Tests Assert.IsTrue(host.IsRunning); Assert.IsTrue(host.IsModuleActive("app.a")); Assert.IsTrue(host.IsModuleActive("app.b"), "依赖模块在提供者激活后自动激活"); - Assert.IsNotNull(started); - CollectionAssert.AreEquivalent(new[] { "app.a", "app.b" }, started!.ModuleIds); + Assert.IsNotNull(probe.Value); + CollectionAssert.AreEquivalent(new[] { "app.a", "app.b" }, probe.Value!.ModuleIds); } [Test] diff --git a/Assets/Modules/ShrinkContext.EventBusAdapter/Runtime/ShrinkEventBusEffects.cs b/Assets/Modules/ShrinkContext.EventBusAdapter/Runtime/ShrinkEventBusEffects.cs index c3211de..b7323ad 100644 --- a/Assets/Modules/ShrinkContext.EventBusAdapter/Runtime/ShrinkEventBusEffects.cs +++ b/Assets/Modules/ShrinkContext.EventBusAdapter/Runtime/ShrinkEventBusEffects.cs @@ -7,48 +7,25 @@ using ShrinkEventBus; namespace ShrinkContext.EventBusAdapter { /// - /// 把 ShrinkEventBus 的订阅/注册包装为可逆效应: - /// IShrinkEventSubscription.Dispose 天然就是订阅的逆操作—— - /// 组件在 apply 里订阅,停用时随上下文自动退订,无需手写卸载路径。 + /// 把生成的 ShrinkEventBus 绑定包装为可逆效应。 /// public static class ShrinkEventBusEffects { - /// 订阅事件即效应:ctx 卸载或手动 Dispose 句柄时自动退订。 - public static ShrinkEffectHandle EffectSubscribe( - this ShrinkCtx ctx, - Action handler, - EventPriority priority = EventPriority.NORMAL, - bool receiveCanceled = false) - where TEvent : EventBase - { - if (ctx == null) - throw new ArgumentNullException(nameof(ctx)); - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - var subscription = EventBus.SubscribeEvent(handler, priority, receiveCanceled); - return ctx.EffectInverse(() => - { - subscription.Dispose(); - return UniTask.CompletedTask; - }); - } - /// - /// 实例对象整体注册即效应:扫描对象上的 [EventSubscribe] 方法(含未织入的非 MonoBehaviour 类), - /// 逆操作为 Unregister。 + /// 激活生成的 [ShrinkSubscribe] 绑定,并在 Context 回滚时统一释放。 /// - public static ShrinkEffectHandle EffectRegister(this ShrinkCtx ctx, object target) + public static ShrinkEffectHandle EffectAttach(this ShrinkCtx ctx, object target, + ShrinkBusKey? defaultBus = null) { if (ctx == null) throw new ArgumentNullException(nameof(ctx)); if (target == null) throw new ArgumentNullException(nameof(target)); - EventBus.Register(target); + var binding = EventBus.Attach(target, defaultBus); return ctx.EffectInverse(() => { - EventBus.Unregister(target); + binding.Dispose(); return UniTask.CompletedTask; }); } diff --git a/Assets/Modules/ShrinkContext.EventBusAdapter/Tests/EventBusEffectTests.cs b/Assets/Modules/ShrinkContext.EventBusAdapter/Tests/EventBusEffectTests.cs index d439fe8..7de509e 100644 --- a/Assets/Modules/ShrinkContext.EventBusAdapter/Tests/EventBusEffectTests.cs +++ b/Assets/Modules/ShrinkContext.EventBusAdapter/Tests/EventBusEffectTests.cs @@ -1,5 +1,4 @@ #nullable enable -using System; using System.Collections.Generic; using Cysharp.Threading.Tasks; using NUnit.Framework; @@ -9,7 +8,7 @@ using UnityEngine; namespace ShrinkContext.EventBusAdapter.Tests { - public sealed class PingEvent : EventBase + public sealed class PingEvent : IShrinkEvent { public int Value { get; set; } } @@ -33,64 +32,74 @@ namespace ShrinkContext.EventBusAdapter.Tests public void TearDown() { _runtime.ShutdownAsync().GetAwaiter().GetResult(); - EventBus.UnregisterAllEvents(); } [Test] - public void EffectSubscribe_ReceivesEvents_AndAutoUnsubscribesOnContextDispose() + public void EffectAttach_ReceivesEvents_AndAutoUnsubscribesOnContextDispose() { var received = new List(); - _ctx.EffectSubscribe(e => received.Add(e.Value)); + var subscriber = new InstanceSubscriber(received); + _ctx.EffectAttach(subscriber); - EventBus.TriggerEvent(new PingEvent { Value = 1 }); - EventBus.TriggerEvent(new PingEvent { Value = 2 }); + EventBus.Post(new PingEvent { Value = 1 }); + EventBus.Post(new PingEvent { Value = 2 }); CollectionAssert.AreEqual(new[] { 1, 2 }, received); _ctx.DisposeAsync().GetAwaiter().GetResult(); - EventBus.TriggerEvent(new PingEvent { Value = 3 }); + EventBus.Post(new PingEvent { Value = 3 }); CollectionAssert.AreEqual(new[] { 1, 2 }, received, - "上下文回滚后订阅必须自动退订(IShrinkEventSubscription.Dispose 即逆操作)"); + "上下文回滚后 EventBus binding 必须通过 IDisposable.Dispose 自动退订"); } [Test] - public void EffectSubscribe_ManualDispose_OnlyUnsubscribesOwn() + public void EffectAttach_ManualDispose_OnlyUnsubscribesOwn() { var first = new List(); var second = new List(); - _ctx.EffectSubscribe(e => first.Add(e.Value)); - var handle = _ctx.EffectSubscribe(e => second.Add(e.Value)); + _ctx.EffectAttach(new InstanceSubscriber(first)); + var handle = _ctx.EffectAttach(new InstanceSubscriber(second)); handle.DisposeAsync().GetAwaiter().GetResult(); - EventBus.TriggerEvent(new PingEvent { Value = 7 }); + EventBus.Post(new PingEvent { Value = 7 }); CollectionAssert.AreEqual(new[] { 7 }, first, "手动退订只影响自己的订阅"); Assert.IsEmpty(second); } [Test] - public void EffectRegister_ObjectSubscribers_AutoUnregisterOnContextDispose() + public void EffectAttach_ObjectSubscribers_AutoUnregisterOnContextDispose() { - var subscriber = new InstanceSubscriber(); - _ctx.EffectRegister(subscriber); + var values = new List(); + var subscriber = new InstanceSubscriber(values); + _ctx.EffectAttach(subscriber); - EventBus.TriggerEvent(new PingEvent { Value = 5 }); + EventBus.Post(new PingEvent { Value = 5 }); Assert.AreEqual(5, subscriber.LastValue); _ctx.DisposeAsync().GetAwaiter().GetResult(); - EventBus.TriggerEvent(new PingEvent { Value = 6 }); + EventBus.Post(new PingEvent { Value = 6 }); Assert.AreEqual(5, subscriber.LastValue, "实例注册随上下文回滚自动注销"); } + [ShrinkEventSubscriber(DefaultBus = "game")] private sealed class InstanceSubscriber { + private readonly List _values; + + public InstanceSubscriber(List values) + { + _values = values; + } + public int LastValue { get; private set; } - [EventSubscribe] + [ShrinkSubscribe] public void OnPing(PingEvent e) { LastValue = e.Value; + _values.Add(e.Value); } } } diff --git a/Assets/Modules/ShrinkContext.EventBusAdapter/package.json b/Assets/Modules/ShrinkContext.EventBusAdapter/package.json index 6a26173..a91be32 100644 --- a/Assets/Modules/ShrinkContext.EventBusAdapter/package.json +++ b/Assets/Modules/ShrinkContext.EventBusAdapter/package.json @@ -6,7 +6,7 @@ "unity": "2022.3", "dependencies": { "com.cneicy.shrink-context-core": "0.1.0", - "com.cneicy.shrink-eventbus": "1.3.0", + "com.cneicy.shrink-eventbus": "2.0.0", "com.cysharp.unitask": "2.5.10" }, "keywords": [ @@ -19,4 +19,4 @@ "name": "cneicy", "url": "https://github.com/cneicy" } -} \ No newline at end of file +} diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEventBusBridge.cs b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEventBusBridge.cs index 98dbfb4..2336742 100644 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEventBusBridge.cs +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEventBusBridge.cs @@ -54,13 +54,13 @@ namespace ShrinkDataSaver.Integration } private static void HandleSettingsChanged(string key, object value) => - EventBus.TriggerEvent(new SettingsChangedEvent { Key = key, Value = value }); + EventBus.Post(new SettingsChangedEvent { Key = key, Value = value }); private static void HandleSaveStarted(SaveStartedEventArgs args) => - EventBus.TriggerEvent(new SaveStartedEvent { SlotIndex = args.SlotIndex, Timestamp = args.Timestamp }); + EventBus.Post(new SaveStartedEvent { SlotIndex = args.SlotIndex, Timestamp = args.Timestamp }); private static void HandleSaveCompleted(SaveCompletedEventArgs args) => - EventBus.TriggerEvent(new SaveCompletedEvent + EventBus.Post(new SaveCompletedEvent { SlotIndex = args.SlotIndex, ModuleNames = args.ModuleNames, @@ -68,17 +68,17 @@ namespace ShrinkDataSaver.Integration }); private static void HandleSaveFailed(SaveFailedEventArgs args) => - EventBus.TriggerEvent(new SaveFailedEvent + EventBus.Post(new SaveFailedEvent { SlotIndex = args.SlotIndex, ErrorMessage = args.ErrorMessage }); private static void HandleLoadStarted(LoadStartedEventArgs args) => - EventBus.TriggerEvent(new LoadStartedEvent { SlotIndex = args.SlotIndex, Timestamp = args.Timestamp }); + EventBus.Post(new LoadStartedEvent { SlotIndex = args.SlotIndex, Timestamp = args.Timestamp }); private static void HandleLoadCompleted(LoadCompletedEventArgs args) => - EventBus.TriggerEvent(new LoadCompletedEvent + EventBus.Post(new LoadCompletedEvent { SlotIndex = args.SlotIndex, ModuleNames = args.ModuleNames, @@ -87,14 +87,14 @@ namespace ShrinkDataSaver.Integration }); private static void HandleLoadFailed(LoadFailedEventArgs args) => - EventBus.TriggerEvent(new LoadFailedEvent + EventBus.Post(new LoadFailedEvent { SlotIndex = args.SlotIndex, ErrorMessage = args.ErrorMessage }); private static void HandleMigrationCompleted(MigrationCompletedEventArgs args) => - EventBus.TriggerEvent(new MigrationCompletedEvent + EventBus.Post(new MigrationCompletedEvent { SlotIndex = args.SlotIndex, FromVersion = args.FromVersion, @@ -102,6 +102,6 @@ namespace ShrinkDataSaver.Integration }); private static void HandleSlotDeleted(SlotDeletedEventArgs args) => - EventBus.TriggerEvent(new SlotDeletedEvent { SlotIndex = args.SlotIndex }); + EventBus.Post(new SlotDeletedEvent { SlotIndex = args.SlotIndex }); } } diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEvents.cs b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEvents.cs index ba521bd..0eb22e3 100644 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEvents.cs +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/DataSaverEvents.cs @@ -3,39 +3,39 @@ using ShrinkEventBus; namespace ShrinkDataSaver.Integration { - public class SettingsChangedEvent : EventBase + public class SettingsChangedEvent : IShrinkEvent { public string Key { get; internal set; } public object Value { get; internal set; } public T Get() => (T)Convert.ChangeType(Value, typeof(T)); } - public class SaveStartedEvent : EventBase + public class SaveStartedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public long Timestamp { get; internal set; } } - public class SaveCompletedEvent : EventBase + public class SaveCompletedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public string[] ModuleNames { get; internal set; } public long Timestamp { get; internal set; } } - public class SaveFailedEvent : EventBase + public class SaveFailedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public string ErrorMessage { get; internal set; } } - public class LoadStartedEvent : EventBase + public class LoadStartedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public long Timestamp { get; internal set; } } - public class LoadCompletedEvent : EventBase + public class LoadCompletedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public string[] ModuleNames { get; internal set; } @@ -43,21 +43,21 @@ namespace ShrinkDataSaver.Integration public long Timestamp { get; internal set; } } - public class LoadFailedEvent : EventBase + public class LoadFailedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public string ErrorMessage { get; internal set; } } - public class MigrationCompletedEvent : EventBase + public class MigrationCompletedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } public int FromVersion { get; internal set; } public int ToVersion { get; internal set; } } - public class SlotDeletedEvent : EventBase + public class SlotDeletedEvent : IShrinkEvent { public int SlotIndex { get; internal set; } } -} \ No newline at end of file +} diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/README.md b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/README.md index e4151b6..85275ae 100644 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/README.md +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/README.md @@ -1,155 +1,39 @@ # ShrinkDataSaver.Integration.EventBus -[ShrinkDataSaver](https://github.com/cneicy/ShrinkDataSaver) 与 [ShrinkEventBus](https://github.com/cneicy/ShrinkEventBus) 的桥接层。 +把 ShrinkDataSaver 的设置、保存、加载、迁移和删除生命周期发布为 ShrinkEventBus 2.0 事件。 -ContextLoader 下由 `ShrinkDataSaverEventBusComponent` 注入 `shrink.service.datasaver` 后接桥,依赖撤回时拆桥;独立使用时调用 `DataSaverEventBusBridge.Register()` / `Unregister()`。仅安装程序集不会再产生全局副作用。 +ContextLoader 通过 `ShrinkDataSaverEventBusComponent` 管理桥生命周期;独立宿主调用 `DataSaverEventBusBridge.Register()`,退出时调用 `Unregister()`。 -## ✨ 特性概览 +事件均实现 `IShrinkEvent`: -| 特性 | 说明 | -|------|------| -| 🌉 **可逆桥接** | 由 Cordis 组件或显式 API 管理注册与注销 | -| 📡 **完整事件覆盖** | 9 种事件类型,覆盖设置变更 + 存档保存/加载/删除/迁移全生命周期 | -| 🔒 **类型安全** | 所有事件继承 `EventBase`,支持 `[EventBusSubscriber]` 自动注册 | -| 📦 **丰富载荷** | 事件携带 SlotIndex、ModuleNames、Version、Timestamp、ErrorMessage 等完整数据 | +- `SettingsChangedEvent` +- `SaveStartedEvent` / `SaveCompletedEvent` / `SaveFailedEvent` +- `LoadStartedEvent` / `LoadCompletedEvent` / `LoadFailedEvent` +- `MigrationCompletedEvent` +- `SlotDeletedEvent` -## 📦 依赖 - -- [ShrinkDataSaver](https://github.com/cneicy/ShrinkDataSaver) `2.0.0+` -- [ShrinkEventBus](https://github.com/cneicy/ShrinkEventBus) `1.0.0+` - -## ⚙️ 安装 - -在项目的 `Packages/manifest.json` 中添加: - -```json -{ - "dependencies": { - "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask", - "com.cneicy.shrink-datasaver": "https://github.com/cneicy/ShrinkDataSaver.git", - "com.cneicy.shrink-datasaver-integration-eventbus": "https://github.com/cneicy/ShrinkDataSaver.Integration.EventBus.git" - } -} -``` - -或通过 Package Manager → `+` → `Add package from git URL` 输入: - -``` -https://github.com/cneicy/ShrinkDataSaver.Integration.EventBus.git -``` - -## 📡 事件类型 - -| 事件类 | 对应原生事件 | 载荷字段 | -|--------|-------------|----------| -| `SettingsChangedEvent` | `ShrinkSettings.OnChanged` | Key, Value | -| `SaveStartedEvent` | `ShrinkSave.OnSaveStarted` | SlotIndex, Timestamp | -| `SaveCompletedEvent` | `ShrinkSave.OnSaveCompleted` | SlotIndex, ModuleNames[], Timestamp | -| `SaveFailedEvent` | `ShrinkSave.OnSaveFailed` | SlotIndex, ErrorMessage | -| `LoadStartedEvent` | `ShrinkSave.OnLoadStarted` | SlotIndex, Timestamp | -| `LoadCompletedEvent` | `ShrinkSave.OnLoadCompleted` | SlotIndex, ModuleNames[], Version, Timestamp | -| `LoadFailedEvent` | `ShrinkSave.OnLoadFailed` | SlotIndex, ErrorMessage | -| `MigrationCompletedEvent` | `ShrinkSave.OnMigrationCompleted` | SlotIndex, FromVersion, ToVersion | -| `SlotDeletedEvent` | `ShrinkSave.OnDeleteCompleted` | SlotIndex | - -## 🚀 使用示例 - -### 使用 `[EventBusSubscriber]` 自动注册(推荐) +订阅只使用 2.0 特性: ```csharp -[EventBusSubscriber] -public class SaveUIManager : MonoBehaviour +[ShrinkEventSubscriber(DefaultBus = "game")] +public sealed class SaveUiHandlers { - [EventSubscribe(EventPriority.NORMAL)] - private void OnSaveCompleted(SaveCompletedEvent e) + [ShrinkSubscribe] + private void OnSaved(SaveCompletedEvent value) { - Debug.Log($"槽位 {e.SlotIndex} 保存成功,模块: {string.Join(", ", e.ModuleNames)}"); - ShowSaveIndicator(); + Debug.Log($"slot {value.SlotIndex} saved"); } - [EventSubscribe(EventPriority.NORMAL)] - private void OnLoadCompleted(LoadCompletedEvent e) + [ShrinkSubscribe(Priority = ShrinkEventPriority.Low)] + private void OnLoadFailed(LoadFailedEvent value) { - Debug.Log($"槽位 {e.SlotIndex} 加载完成 (v{e.Version})"); - TransitionToGame(); - } - - [EventSubscribe(EventPriority.NORMAL)] - private void OnSaveFailed(SaveFailedEvent e) - { - ShowErrorDialog($"保存失败: {e.ErrorMessage}"); - } - - [EventSubscribe(EventPriority.NORMAL)] - private void OnLoadFailed(LoadFailedEvent e) - { - ShowErrorDialog($"加载失败: {e.ErrorMessage}"); - } - - [EventSubscribe(EventPriority.NORMAL)] - private void OnMigration(MigrationCompletedEvent e) - { - Debug.Log($"存档已从 v{e.FromVersion} 迁移到 v{e.ToVersion}"); - } - - [EventSubscribe(EventPriority.NORMAL)] - private void OnSettingsChanged(SettingsChangedEvent e) - { - if (e.Key == "MasterVolume") - ApplyVolume(e.Get()); + Debug.LogError(value.ErrorMessage); } } + +using var binding = EventBus.Attach(new SaveUiHandlers()); ``` -### 手动注册 +桥接方向是 DataSaver -> EventBus,发布使用 `EventBus.Post(...)`。没有手工 Delegate 注册或旧 Trigger API。 -```csharp -public class AnalyticsTracker : IDisposable -{ - public AnalyticsTracker() - { - EventBus.RegisterEvent(OnSave, EventPriority.LOWEST); - EventBus.RegisterEvent(OnLoad, EventPriority.LOWEST); - } - - private void OnSave(SaveCompletedEvent e) - => Analytics.Track("save", new { slot = e.SlotIndex, modules = e.ModuleNames.Length }); - - private void OnLoad(LoadCompletedEvent e) - => Analytics.Track("load", new { slot = e.SlotIndex, version = e.Version }); - - public void Dispose() - => EventBus.UnregisterAllEventsForObject(this); -} -``` - -## 🏗️ 架构 - -``` -ShrinkDataSaver.Integration.EventBus/ -├── DataSaverEvents.cs 9 个 EventBase 子类(事件定义) -├── DataSaverEventBusBridge.cs 显式、可注销的静态桥接器 -├── ShrinkDataSaverEventBusComponent.cs 按服务键管理桥生命周期 -└── ShrinkDataSaver.Integration.EventBus.asmdef -``` - -**桥接原理:** - -``` -ShrinkDataSaver 原生事件 (Action) - └─ ShrinkDataSaverEventBusComponent / 显式 Register - └─ EventBus.TriggerEvent(new XxxEvent { ... }) - └─ ShrinkEventBus 分发到所有订阅者 -``` - -桥接器是单向的:DataSaver → EventBus。业务代码只需订阅 EventBus 事件,无需直接引用 ShrinkDataSaver 的原生事件。 - -## ⚠️ 注意事项 - -- **生命周期**:ContextLoader 组合根负责接桥;Standalone 必须在 DataSaver 初始化后显式 `Register()`,并在退出时 `Unregister()`。 -- **不影响原生事件**:桥接是附加行为,ShrinkDataSaver 的原生 C# 事件仍然正常触发,两种订阅方式可并存。 -- **SettingsChangedEvent.Get\()**:提供泛型辅助方法获取强类型值,内部使用 `Convert.ChangeType`。 - -## 📄 License - -[MIT](LICENSE) +`SettingsChangedEvent.Get()` 内部使用 `Convert.ChangeType`,只应在设置值确实可转换时调用。 diff --git a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/package.json b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/package.json index 55d7977..8a9baf9 100644 --- a/Assets/Modules/ShrinkDataSaver.Integration.EventBus/package.json +++ b/Assets/Modules/ShrinkDataSaver.Integration.EventBus/package.json @@ -11,7 +11,7 @@ "com.cneicy.shrink-datasaver": "2.2.0", "com.cneicy.shrink-context-core": "0.1.0", "com.cysharp.unitask": "2.5.10", - "com.cneicy.shrink-eventbus": "1.3.0" + "com.cneicy.shrink-eventbus": "2.0.0" }, "keywords": ["save", "eventbus", "integration", "bridge"], "author": { diff --git a/Assets/Modules/ShrinkDataSaver/README.md b/Assets/Modules/ShrinkDataSaver/README.md index 8cea6e6..0d34d1a 100644 --- a/Assets/Modules/ShrinkDataSaver/README.md +++ b/Assets/Modules/ShrinkDataSaver/README.md @@ -279,25 +279,25 @@ if (meta.IsEncrypted) > 详细文档见 [ShrinkDataSaver.Integration.EventBus](https://github.com/cneicy/ShrinkDataSaver.Integration.EventBus) -项目中同时包含 `ShrinkDataSaver.Integration.EventBus` 时,桥接器会在游戏启动时自动初始化,将所有原生事件映射到 EventBus: +项目中同时包含 `ShrinkDataSaver.Integration.EventBus` 时,由 Context 组件或宿主显式管理桥接生命周期,将原生事件发布到指定的 ShrinkEventBus 2.0 Bus: ```csharp -[EventBusSubscriber] -public class SaveUIManager : MonoBehaviour +[ShrinkEventSubscriber(DefaultBus = "game")] +public sealed partial class SaveUIManager : MonoBehaviour { - [EventSubscribe(EventPriority.NORMAL)] + [ShrinkSubscribe] private void OnSaveCompleted(SaveCompletedEvent e) { ShowSaveIndicator(e.SlotIndex, e.ModuleNames); } - [EventSubscribe(EventPriority.NORMAL)] + [ShrinkSubscribe] private void OnLoadFailed(LoadFailedEvent e) { ShowErrorDialog(e.ErrorMessage); } - [EventSubscribe(EventPriority.NORMAL)] + [ShrinkSubscribe] private void OnSettingsChanged(SettingsChangedEvent e) { if (e.Key == "MasterVolume") @@ -306,6 +306,8 @@ public class SaveUIManager : MonoBehaviour } ``` +普通 C# 对象通过 `EventBus.Attach(target)` 接入;MonoBehaviour 可使用 `ShrinkMonoEventScope`,也可在 `OnEnable`/`OnDisable` 中自行持有并释放 binding。桥接器本身由 `ShrinkDataSaverEventBusComponent` 或 `DataSaverEventBusBridge.Register()`/`Unregister()` 管理。 + **EventBus 事件类型完整列表:** `SettingsChangedEvent` · `SaveStartedEvent` · `SaveCompletedEvent` · `SaveFailedEvent` · `LoadStartedEvent` · `LoadCompletedEvent` · `LoadFailedEvent` · `MigrationCompletedEvent` · `SlotDeletedEvent` diff --git a/Assets/Modules/ShrinkEventBus/CHANGELOG.md b/Assets/Modules/ShrinkEventBus/CHANGELOG.md index 179bd90..06146d5 100644 --- a/Assets/Modules/ShrinkEventBus/CHANGELOG.md +++ b/Assets/Modules/ShrinkEventBus/CHANGELOG.md @@ -2,6 +2,26 @@ 本文件记录 `ShrinkEventBus` 在当前工作区中的包内变更。 +## [2.0.0] - 2026-08-23 + +### Added + +- 新增统一 `IShrinkEvent` 契约、`ShrinkEventSubscriber` / `ShrinkSubscribe` 特性与 `Post` / `PostAsync` 入口。 +- 新增命名多 Bus:Game、Scene、Mod、World、Server 及自定义 `ShrinkBusKey`。 +- Bus 创建时可选择 Inline、Unity MainThread、DedicatedThread 或 TaskPool 调度器,并支持有界队列、溢出策略、最大并发与关闭排空。 +- Unity 中异步 handler 与调度优先使用 UniTask;新增 `ShrinkMonoEventScope` 管理普通 MonoBehaviour 的绑定生命周期。 +- ILPostProcessor 为特性标记的普通 C# / Mono 类型生成 `IShrinkGeneratedSubscriber` 强类型绑定,发布热路径不使用反射调用。 +- 新增可选 `com.cneicy.shrink-eventbus-entities` 包,Burst Job 通过 NativeQueue writer/playback 进入同一 Bus。 +- 新增 `DotNet/ShrinkEventBus.Core` ValueTask 运行时与 Roslyn incremental generator,纯 .NET 宿主无需 Unity 或 UniTask。 +- EventBus 可视化调试器使用 UI Toolkit 重写,新增 Bus 导航、实时指标、分发耗时、线程/执行模式/结果详情、问题筛选、跟随和 CSV 导出;详细采样只在窗口主动采集时启用。 +- 新增独立 Play Mode benchmark,覆盖 0/1/8/30 handler、取消、异步和生成绑定开销。 +- Inline Bus 使用专用运行时实现,非取消全同步 channel 在订阅变化时生成有序 multicast dispatcher;`ShrinkPostResult` 压缩为 32-bit 状态,保持 API 不变。 + +### Breaking + +- 删除 `EventBase`、`EventBusSubscriber` / `EventSubscribe`、`RegisterEvent` / `SubscribeEvent` / `TriggerEvent`、手工 Delegate 注册和旧反射通道。 +- 所有事件改为实现 `IShrinkEvent`;取消和结果分别实现 `IShrinkCancelableEvent`、`IShrinkResultEvent`。 + ## [1.3.0] - 2026-06-12 ### Breaking diff --git a/Assets/Modules/ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs b/Assets/Modules/ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs index 7881503..f139d7d 100644 --- a/Assets/Modules/ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs +++ b/Assets/Modules/ShrinkEventBus/CodeGen/Editor/EventBusILPostProcessor.cs @@ -16,13 +16,6 @@ namespace ShrinkEventBus.CodeGen public sealed class EventBusILPostProcessor : ILPostProcessor { private const string RuntimeAssemblyName = "ShrinkEventBus.Runtime"; - private static readonly string[] SharedCoverageAssemblyNames = - { - "ShrinkCommand.Runtime", - "ShrinkNetwork.Runtime", - "ShrinkApp.Core.Runtime" - }; - public override ILPostProcessor GetInstance() => this; public override bool WillProcess(ICompiledAssembly compiledAssembly) @@ -30,9 +23,6 @@ namespace ShrinkEventBus.CodeGen if (!ReferencesAssembly(compiledAssembly, RuntimeAssemblyName)) return false; - if (ShouldDeferToSharedCodeGen(compiledAssembly)) - return false; - return true; } @@ -47,54 +37,28 @@ namespace ShrinkEventBus.CodeGen try { - var subscriberType = FindType(module, "ShrinkEventBus.EventBusSubscriberAttribute", RuntimeAssemblyName); - var subscribeAttributeType = FindType(module, "ShrinkEventBus.EventSubscribeAttribute", RuntimeAssemblyName); - var eventBusType = FindType(module, "ShrinkEventBus.EventBus", RuntimeAssemblyName); - var staticRegistryCtor = FindTypeArrayConstructor( - module, - "ShrinkEventBus.EventBusStaticRegistryAttribute", - RuntimeAssemblyName); - if (subscriberType == null || subscribeAttributeType == null || eventBusType == null) + var generatedSubscriberType = FindType(module, "ShrinkEventBus.ShrinkEventSubscriberAttribute", RuntimeAssemblyName); + var generatedSubscribeType = FindType(module, "ShrinkEventBus.ShrinkSubscribeAttribute", RuntimeAssemblyName); + if (generatedSubscriberType == null || generatedSubscribeType == null) return GetResult(assemblyDefinition, diagnostics); - var eventBusTypeDef = eventBusType.Resolve(); - var autoRegisterMethod = eventBusTypeDef?.Methods.FirstOrDefault(method => - method.Name == "AutoRegister" && - method.IsStatic && - method.Parameters.Count == 1); - var unregisterMethod = eventBusTypeDef?.Methods.FirstOrDefault(method => - method.Name == "UnregisterInstance" && - method.IsStatic && - method.Parameters.Count == 1); - if (autoRegisterMethod == null || unregisterMethod == null) - return GetResult(assemblyDefinition, diagnostics); + foreach (var type in GetAllTypes(module.Types) + .Where(type => !type.IsInterface && !type.IsAbstract) + .Where(type => HasAttribute(type, generatedSubscriberType)) + .Where(type => type.Methods.Any(method => + !method.IsStatic && HasAttribute(method, generatedSubscribeType)))) + { + InjectGeneratedBinding(type, module, generatedSubscribeType); + } - var autoRegisterMethodRef = module.ImportReference(autoRegisterMethod); - var unregisterMethodRef = module.ImportReference(unregisterMethod); - - var subscriberTypes = module.Types - .Where(type => HasAttribute(type, subscriberType)) - .Where(InheritsFromMonoBehaviour) - .Where(type => HasInstanceSubscribeMethod(type, subscribeAttributeType)) + var staticSubscriberTypes = GetAllTypes(module.Types) + .Where(type => HasAttribute(type, generatedSubscriberType)) + .Where(type => type.Methods.Any(method => + method.IsStatic && HasAttribute(method, generatedSubscribeType))) .ToArray(); + if (staticSubscriberTypes.Length > 0) + InjectStaticBootstrap(module, staticSubscriberTypes, generatedSubscribeType); - foreach (var type in subscriberTypes) - { - InjectAutoRegister(type, module, autoRegisterMethodRef); - InjectAutoUnregister(type, module, unregisterMethodRef); - } - - if (staticRegistryCtor != null) - { - var staticSubscriberTypes = module.Types - .Where(type => HasAttribute(type, subscriberType)) - .Where(type => type.Methods.Any(method => - method.IsStatic && HasAttribute(method, subscribeAttributeType))) - .Select(type => module.ImportReference(type)) - .ToArray(); - if (staticSubscriberTypes.Length > 0) - AddAssemblyTypeArrayAttribute(module, staticRegistryCtor, staticSubscriberTypes); - } } catch (Exception ex) { @@ -108,25 +72,6 @@ namespace ShrinkEventBus.CodeGen return GetResult(assemblyDefinition, diagnostics); } - private static bool ShouldDeferToSharedCodeGen(ICompiledAssembly compiledAssembly) - { - if (!IsSharedCodeGenAvailable()) - return false; - - return SharedCoverageAssemblyNames.Any(name => ReferencesAssembly(compiledAssembly, name)); - } - - private static bool IsSharedCodeGenAvailable() - { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (string.Equals(assembly.GetName().Name, "Unity.ShrinkShared.CodeGen", StringComparison.Ordinal)) - return true; - } - - return Type.GetType("ShrinkShared.CodeGen.ShrinkRegistryILPostProcessor, Unity.ShrinkShared.CodeGen", false) != null; - } - private static bool ReferencesAssembly(ICompiledAssembly compiledAssembly, string assemblyName) { return compiledAssembly.References.Any(reference => @@ -138,178 +83,409 @@ namespace ShrinkEventBus.CodeGen return provider.CustomAttributes.Any(attribute => attribute.AttributeType.FullName == expectedAttributeType.FullName); } - private static bool HasInstanceSubscribeMethod(TypeDefinition type, TypeReference subscribeAttributeType) + private static IEnumerable GetAllTypes(IEnumerable roots) { - var current = type; - while (current != null && current.Name != "MonoBehaviour") + foreach (var type in roots) { - if (current.Methods.Any(method => !method.IsStatic && HasAttribute(method, subscribeAttributeType))) - return true; - - var baseTypeRef = current.BaseType; - if (baseTypeRef == null) - return false; - - TypeDefinition? resolvedBase; - try - { - resolvedBase = baseTypeRef.Resolve(); - } - catch - { - return true; - } - - if (resolvedBase == null) - return true; - - current = resolvedBase; + yield return type; + foreach (var nested in GetAllTypes(type.NestedTypes)) + yield return nested; } - - return false; } - private static bool InheritsFromMonoBehaviour(TypeDefinition type) + private static void InjectGeneratedBinding(TypeDefinition type, ModuleDefinition module, + TypeReference subscribeAttributeType) { - var current = type.BaseType; - while (current != null) - { - if (current.Name == "MonoBehaviour") - return true; + var generatedInterface = FindType(module, + "ShrinkEventBus.IShrinkGeneratedSubscriber", RuntimeAssemblyName) + ?? throw new InvalidOperationException("IShrinkGeneratedSubscriber was not found."); + if (type.Interfaces.Any(item => item.InterfaceType.FullName == generatedInterface.FullName)) + return; - try + var resolverType = FindType(module, "ShrinkEventBus.IShrinkBusResolver", RuntimeAssemblyName) + ?? throw new InvalidOperationException("IShrinkBusResolver was not found."); + var busKeyType = FindType(module, "ShrinkEventBus.ShrinkBusKey", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkBusKey was not found."); + var bindingType = FindType(module, "ShrinkEventBus.ShrinkEventBinding", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkEventBinding was not found."); + var bindingHelperType = FindType(module, "ShrinkEventBus.ShrinkGeneratedBinding", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkGeneratedBinding was not found."); + var priorityType = FindType(module, "ShrinkEventBus.ShrinkEventPriority", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkEventPriority was not found."); + var asyncHandlerType = FindType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkAsyncEventHandler was not found."); + + var nullableBusKey = new GenericInstanceType(module.ImportReference(typeof(Nullable<>))); + nullableBusKey.GenericArguments.Add(busKeyType); + var disposableType = module.ImportReference(typeof(IDisposable)); + var bindingDefinition = bindingType.Resolve() + ?? throw new InvalidOperationException("ShrinkEventBinding could not be resolved."); + var bindingCtor = module.ImportReference(bindingDefinition.Methods.Single(method => + method.IsConstructor && method.Parameters.Count == 0)); + var bindingAdd = module.ImportReference(bindingDefinition.Methods.Single(method => + method.Name == "Add" && method.Parameters.Count == 1)); + + var helperDefinition = bindingHelperType.Resolve() + ?? throw new InvalidOperationException("ShrinkGeneratedBinding could not be resolved."); + var syncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method => + method.Name == "Subscribe" && method.HasGenericParameters)); + var asyncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method => + method.Name == "SubscribeAsync" && method.HasGenericParameters)); + var legacyAsyncSubscribe = module.ImportReference(helperDefinition.Methods.Single(method => + method.Name == "SubscribeAsyncLegacy" && method.HasGenericParameters)); + + var interfaceDefinition = generatedInterface.Resolve() + ?? throw new InvalidOperationException("IShrinkGeneratedSubscriber could not be resolved."); + var interfaceMethod = module.ImportReference(interfaceDefinition.Methods.Single(method => + method.Name == "AttachGenerated")); + + var generatedMethod = new MethodDefinition( + "ShrinkEventBus.IShrinkGeneratedSubscriber.AttachGenerated", + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.HideBySig | + MethodAttributes.NewSlot | MethodAttributes.Virtual, + disposableType); + generatedMethod.Parameters.Add(new ParameterDefinition("resolver", ParameterAttributes.None, resolverType)); + generatedMethod.Parameters.Add(new ParameterDefinition("defaultBus", ParameterAttributes.Optional, + nullableBusKey)); + generatedMethod.Overrides.Add(interfaceMethod); + generatedMethod.Body.InitLocals = true; + var bindingLocal = new VariableDefinition(bindingType); + generatedMethod.Body.Variables.Add(bindingLocal); + var il = generatedMethod.Body.GetILProcessor(); + il.Emit(OpCodes.Newobj, bindingCtor); + il.Emit(OpCodes.Stloc, bindingLocal); + + var classDefaultBus = ReadStringProperty( + type.CustomAttributes.First(attribute => + attribute.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"), + "DefaultBus"); + + foreach (var handler in type.Methods.Where(method => + !method.IsStatic && HasAttribute(method, subscribeAttributeType)).ToArray()) + { + EmitGeneratedSubscription(il, module, type, handler, + handler.CustomAttributes.First(attribute => + attribute.AttributeType.FullName == subscribeAttributeType.FullName), + classDefaultBus, bindingLocal, bindingAdd, syncSubscribe, asyncSubscribe, + legacyAsyncSubscribe, asyncHandlerType); + } + + il.Emit(OpCodes.Ldloc, bindingLocal); + il.Emit(OpCodes.Ret); + type.Interfaces.Add(new InterfaceImplementation(generatedInterface)); + type.Methods.Add(generatedMethod); + } + + private static void EmitGeneratedSubscription(ILProcessor il, ModuleDefinition module, + TypeDefinition ownerType, MethodDefinition handler, CustomAttribute attribute, + string classDefaultBus, VariableDefinition bindingLocal, MethodReference bindingAdd, + MethodReference syncSubscribe, MethodReference asyncSubscribe, + MethodReference legacyAsyncSubscribe, TypeReference asyncHandlerType) + { + if (handler.Parameters.Count is < 1 or > 2) + throw new InvalidOperationException( + $"[ShrinkSubscribe] method {handler.FullName} must have one event parameter and an optional CancellationToken."); + + var eventType = module.ImportReference(handler.Parameters[0].ParameterType); + var eventInterface = FindType(module, "ShrinkEventBus.IShrinkEvent", RuntimeAssemblyName) + ?? throw new InvalidOperationException("IShrinkEvent was not found."); + if (!ImplementsInterface(eventType, eventInterface.FullName)) + throw new InvalidOperationException( + $"[ShrinkSubscribe] method {handler.FullName} event parameter must implement IShrinkEvent."); + + var bus = ReadStringProperty(attribute, "Bus"); + if (string.IsNullOrWhiteSpace(bus)) + bus = classDefaultBus; + var priority = ReadIntProperty(attribute, "Priority", 2); + var numericPriority = ReadIntProperty(attribute, "NumericPriority", 0); + var receiveCanceled = ReadBoolProperty(attribute, "ReceiveCanceled", false); + + MethodReference openSubscribe; + TypeReference delegateType; + if (handler.ReturnType.FullName == module.TypeSystem.Void.FullName && handler.Parameters.Count == 1) + { + openSubscribe = syncSubscribe; + delegateType = MakeGenericType(module, typeof(Action<>), eventType); + } + else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" && + handler.Parameters.Count == 1) + { + openSubscribe = legacyAsyncSubscribe; + var uniTaskType = FindType(module, "Cysharp.Threading.Tasks.UniTask", "UniTask") + ?? module.ImportReference(handler.ReturnType); + delegateType = MakeGenericType(module, typeof(Func<,>), eventType, uniTaskType); + } + else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" && + handler.Parameters.Count == 2 && + handler.Parameters[1].ParameterType.FullName == typeof(System.Threading.CancellationToken).FullName) + { + openSubscribe = asyncSubscribe; + var closedAsyncHandler = new GenericInstanceType(asyncHandlerType); + closedAsyncHandler.GenericArguments.Add(eventType); + delegateType = closedAsyncHandler; + } + else + { + throw new InvalidOperationException( + $"Unsupported [ShrinkSubscribe] signature: {handler.FullName}. Use void(T), UniTask(T), or UniTask(T, CancellationToken)."); + } + + var closedSubscribe = new GenericInstanceMethod(openSubscribe); + closedSubscribe.GenericArguments.Add(eventType); + var delegateCtor = MakeDelegateConstructor(module, delegateType); + + il.Emit(OpCodes.Ldloc, bindingLocal); + il.Emit(OpCodes.Ldarg_1); + il.Emit(OpCodes.Ldarg_2); + il.Emit(OpCodes.Ldstr, bus ?? string.Empty); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldftn, module.ImportReference(handler)); + il.Emit(OpCodes.Newobj, delegateCtor); + il.Emit(OpCodes.Ldc_I4, priority); + il.Emit(OpCodes.Ldc_I4, numericPriority); + il.Emit(receiveCanceled ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Call, closedSubscribe); + il.Emit(OpCodes.Callvirt, bindingAdd); + } + + private static void InjectStaticBootstrap(ModuleDefinition module, + IReadOnlyList subscriberTypes, TypeReference subscribeAttributeType) + { + var registryType = FindType(module, "ShrinkEventBus.ShrinkStaticBindingRegistry", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkStaticBindingRegistry was not found."); + var asyncHandlerType = FindType(module, "ShrinkEventBus.ShrinkAsyncEventHandler`1", RuntimeAssemblyName) + ?? throw new InvalidOperationException("ShrinkAsyncEventHandler was not found."); + var registryDefinition = registryType.Resolve() + ?? throw new InvalidOperationException("ShrinkStaticBindingRegistry could not be resolved."); + var syncRegister = module.ImportReference(registryDefinition.Methods.Single(method => + method.Name == "Register" && method.HasGenericParameters)); + var asyncRegister = module.ImportReference(registryDefinition.Methods.Single(method => + method.Name == "RegisterAsync" && method.HasGenericParameters)); + var legacyAsyncRegister = module.ImportReference(registryDefinition.Methods.Single(method => + method.Name == "RegisterAsyncLegacy" && method.HasGenericParameters)); + + var bootstrap = new TypeDefinition("ShrinkEventBus.Generated", + "ShrinkGeneratedStaticBindings", + TypeAttributes.Abstract | TypeAttributes.Sealed | TypeAttributes.NotPublic, + module.TypeSystem.Object); + module.Types.Add(bootstrap); + + var register = new MethodDefinition("Register", + MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, + module.TypeSystem.Void); + bootstrap.Methods.Add(register); + var il = register.Body.GetILProcessor(); + + foreach (var type in subscriberTypes) + { + var classDefaultBus = ReadStringProperty( + type.CustomAttributes.First(attribute => + attribute.AttributeType.FullName == "ShrinkEventBus.ShrinkEventSubscriberAttribute"), + "DefaultBus"); + foreach (var handler in type.Methods.Where(method => + method.IsStatic && HasAttribute(method, subscribeAttributeType)).ToArray()) { - current = current.Resolve()?.BaseType; - } - catch - { - break; + EmitStaticGeneratedSubscription(il, module, handler, + handler.CustomAttributes.First(attribute => + attribute.AttributeType.FullName == subscribeAttributeType.FullName), + classDefaultBus, syncRegister, asyncRegister, + legacyAsyncRegister, asyncHandlerType); } } - return false; + il.Emit(OpCodes.Ret); + InjectModuleInitializer(module, register); } - private static void InjectAutoRegister(TypeDefinition type, ModuleDefinition module, MethodReference autoRegisterMethodRef) + private static void EmitStaticGeneratedSubscription(ILProcessor il, ModuleDefinition module, + MethodDefinition handler, CustomAttribute attribute, string classDefaultBus, + MethodReference syncRegister, MethodReference asyncRegister, + MethodReference legacyAsyncRegister, TypeReference asyncHandlerType) { - var awake = type.Methods.FirstOrDefault(method => method.Name == "Awake" && !method.IsStatic); - if (awake == null) - { - var baseAwakeRef = FindBaseMethodReference(type, "Awake", module); - if (baseAwakeRef != null) - { - awake = new MethodDefinition("Awake", - MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual, - module.TypeSystem.Void); - var il = awake.Body.GetILProcessor(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, baseAwakeRef); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, autoRegisterMethodRef); - il.Emit(OpCodes.Ret); - type.Methods.Add(awake); - return; - } + if (handler.Parameters.Count is < 1 or > 2) + throw new InvalidOperationException( + $"[ShrinkSubscribe] method {handler.FullName} must have one event parameter and an optional CancellationToken."); - awake = new MethodDefinition("Awake", - MethodAttributes.Private | MethodAttributes.HideBySig, + var eventType = module.ImportReference(handler.Parameters[0].ParameterType); + var eventInterface = FindType(module, "ShrinkEventBus.IShrinkEvent", RuntimeAssemblyName) + ?? throw new InvalidOperationException("IShrinkEvent was not found."); + if (!ImplementsInterface(eventType, eventInterface.FullName)) + throw new InvalidOperationException( + $"[ShrinkSubscribe] method {handler.FullName} event parameter must implement IShrinkEvent."); + + var bus = ReadStringProperty(attribute, "Bus"); + if (string.IsNullOrWhiteSpace(bus)) + bus = classDefaultBus; + var priority = ReadIntProperty(attribute, "Priority", 2); + var numericPriority = ReadIntProperty(attribute, "NumericPriority", 0); + var receiveCanceled = ReadBoolProperty(attribute, "ReceiveCanceled", false); + + MethodReference openRegister; + TypeReference delegateType; + if (handler.ReturnType.FullName == module.TypeSystem.Void.FullName && handler.Parameters.Count == 1) + { + openRegister = syncRegister; + delegateType = MakeGenericType(module, typeof(Action<>), eventType); + } + else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" && + handler.Parameters.Count == 1) + { + openRegister = legacyAsyncRegister; + var uniTaskType = FindType(module, "Cysharp.Threading.Tasks.UniTask", "UniTask") + ?? module.ImportReference(handler.ReturnType); + delegateType = MakeGenericType(module, typeof(Func<,>), eventType, uniTaskType); + } + else if (handler.ReturnType.FullName == "Cysharp.Threading.Tasks.UniTask" && + handler.Parameters.Count == 2 && + handler.Parameters[1].ParameterType.FullName == typeof(System.Threading.CancellationToken).FullName) + { + openRegister = asyncRegister; + var closedAsyncHandler = new GenericInstanceType(asyncHandlerType); + closedAsyncHandler.GenericArguments.Add(eventType); + delegateType = closedAsyncHandler; + } + else + { + throw new InvalidOperationException( + $"Unsupported static [ShrinkSubscribe] signature: {handler.FullName}."); + } + + var closedRegister = new GenericInstanceMethod(openRegister); + closedRegister.GenericArguments.Add(eventType); + var delegateCtor = MakeDelegateConstructor(module, delegateType); + var handlerBridge = CreateStaticHandlerBridge(module, handler); + + il.Emit(OpCodes.Ldstr, bus ?? string.Empty); + il.Emit(OpCodes.Ldnull); + il.Emit(OpCodes.Ldftn, module.ImportReference(handlerBridge)); + il.Emit(OpCodes.Newobj, delegateCtor); + il.Emit(OpCodes.Ldc_I4, priority); + il.Emit(OpCodes.Ldc_I4, numericPriority); + il.Emit(receiveCanceled ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0); + il.Emit(OpCodes.Call, closedRegister); + } + + private static MethodDefinition CreateStaticHandlerBridge(ModuleDefinition module, + MethodDefinition handler) + { + var bridge = new MethodDefinition( + $"ShrinkEventBus.GeneratedStaticHandler_{handler.MetadataToken.ToInt32():X8}", + MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, + module.ImportReference(handler.ReturnType)); + for (var i = 0; i < handler.Parameters.Count; i++) + { + var parameter = handler.Parameters[i]; + bridge.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, + module.ImportReference(parameter.ParameterType))); + } + + var il = bridge.Body.GetILProcessor(); + for (var i = 0; i < bridge.Parameters.Count; i++) + il.Emit(OpCodes.Ldarg, bridge.Parameters[i]); + il.Emit(OpCodes.Call, module.ImportReference(handler)); + il.Emit(OpCodes.Ret); + handler.DeclaringType.Methods.Add(bridge); + return bridge; + } + + private static void InjectModuleInitializer(ModuleDefinition module, MethodReference register) + { + var moduleType = module.Types.First(type => type.Name == ""); + var initializer = moduleType.Methods.FirstOrDefault(method => method.Name == ".cctor"); + if (initializer == null) + { + initializer = new MethodDefinition(".cctor", + MethodAttributes.Private | MethodAttributes.Static | + MethodAttributes.HideBySig | MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName, module.TypeSystem.Void); - var retIl = awake.Body.GetILProcessor(); - retIl.Emit(OpCodes.Ret); - type.Methods.Add(awake); + initializer.Body.GetILProcessor().Emit(OpCodes.Ret); + moduleType.Methods.Add(initializer); } - var processor = awake.Body.GetILProcessor(); - var instructions = new List - { - processor.Create(OpCodes.Ldarg_0), - processor.Create(OpCodes.Call, autoRegisterMethodRef), - processor.Create(OpCodes.Nop) - }; - instructions.Reverse(); - instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); + var processor = initializer.Body.GetILProcessor(); + processor.InsertBefore(initializer.Body.Instructions[0], + processor.Create(OpCodes.Call, register)); } - private static void InjectAutoUnregister(TypeDefinition type, ModuleDefinition module, MethodReference unregisterMethodRef) + private static GenericInstanceType MakeGenericType(ModuleDefinition module, Type openType, + params TypeReference[] arguments) { - var onDestroy = type.Methods.FirstOrDefault(method => method.Name == "OnDestroy" && !method.IsStatic); - if (onDestroy == null) - { - var baseOnDestroyRef = FindBaseMethodReference(type, "OnDestroy", module); - if (baseOnDestroyRef != null) - { - onDestroy = new MethodDefinition("OnDestroy", - MethodAttributes.Family | MethodAttributes.HideBySig | MethodAttributes.Virtual, - module.TypeSystem.Void); - var il = onDestroy.Body.GetILProcessor(); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, unregisterMethodRef); - il.Emit(OpCodes.Ldarg_0); - il.Emit(OpCodes.Call, baseOnDestroyRef); - il.Emit(OpCodes.Ret); - type.Methods.Add(onDestroy); - return; - } - - onDestroy = new MethodDefinition("OnDestroy", - MethodAttributes.Private | MethodAttributes.HideBySig, - module.TypeSystem.Void); - var retIl = onDestroy.Body.GetILProcessor(); - retIl.Emit(OpCodes.Ret); - type.Methods.Add(onDestroy); - } - - var processor = onDestroy.Body.GetILProcessor(); - var instructions = new List - { - processor.Create(OpCodes.Ldarg_0), - processor.Create(OpCodes.Call, unregisterMethodRef), - processor.Create(OpCodes.Nop) - }; - instructions.Reverse(); - instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); + var result = new GenericInstanceType(module.ImportReference(openType)); + foreach (var argument in arguments) + result.GenericArguments.Add(argument); + return result; } - private static MethodReference? FindBaseMethodReference(TypeDefinition type, string methodName, ModuleDefinition module) + private static MethodReference MakeDelegateConstructor(ModuleDefinition module, TypeReference delegateType) { + var ctor = new MethodReference(".ctor", module.TypeSystem.Void, delegateType) + { + HasThis = true, + CallingConvention = MethodCallingConvention.Default + }; + ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object)); + ctor.Parameters.Add(new ParameterDefinition(module.TypeSystem.IntPtr)); + return ctor; + } + + private static bool ImplementsInterface(TypeReference type, string interfaceFullName) + { + TypeDefinition? current; try { - var baseTypeRef = type.BaseType; - while (baseTypeRef != null) - { - var baseTypeDef = baseTypeRef.Resolve(); - if (baseTypeDef == null) - break; - - var method = baseTypeDef.Methods.FirstOrDefault(candidate => - candidate.Name == methodName && !candidate.IsStatic && candidate.IsVirtual); - if (method != null) - { - if (baseTypeRef is GenericInstanceType genericInstance) - { - var methodRef = new MethodReference( - method.Name, - module.ImportReference(method.ReturnType), - module.ImportReference(genericInstance)) - { - HasThis = method.HasThis, - ExplicitThis = method.ExplicitThis, - CallingConvention = method.CallingConvention - }; - return methodRef; - } - - return module.ImportReference(method); - } - - baseTypeRef = baseTypeDef.BaseType; - } + current = type.Resolve(); } catch { + return false; } - return null; + while (current != null) + { + if (current.Interfaces.Any(item => item.InterfaceType.FullName == interfaceFullName)) + return true; + try + { + current = current.BaseType?.Resolve(); + } + catch + { + return false; + } + } + return false; + } + + private static string ReadStringProperty(CustomAttribute attribute, string name) + { + foreach (var property in attribute.Properties) + { + if (property.Name == name) + return property.Argument.Value as string ?? string.Empty; + } + return string.Empty; + } + + private static int ReadIntProperty(CustomAttribute attribute, string name, int defaultValue) + { + foreach (var property in attribute.Properties) + { + if (property.Name == name) + return Convert.ToInt32(property.Argument.Value); + } + return defaultValue; + } + + private static bool ReadBoolProperty(CustomAttribute attribute, string name, bool defaultValue) + { + foreach (var property in attribute.Properties) + { + if (property.Name == name) + return Convert.ToBoolean(property.Argument.Value); + } + return defaultValue; } private static TypeReference? FindType(ModuleDefinition module, string fullName, string assemblyName) @@ -318,28 +494,6 @@ namespace ShrinkEventBus.CodeGen return resolved == null ? null : module.ImportReference(resolved); } - private static MethodReference? FindTypeArrayConstructor(ModuleDefinition module, string fullName, string assemblyName) - { - var typeRef = FindType(module, fullName, assemblyName); - var typeDef = typeRef?.Resolve(); - var ctor = typeDef?.Methods.FirstOrDefault(method => - method.IsConstructor && - method.Parameters.Count == 1 && - method.Parameters[0].ParameterType.IsArray && - method.Parameters[0].ParameterType.GetElementType().FullName == module.ImportReference(typeof(Type)).FullName); - return ctor == null ? null : module.ImportReference(ctor); - } - - private static void AddAssemblyTypeArrayAttribute(ModuleDefinition module, MethodReference ctor, TypeReference[] types) - { - var attribute = new CustomAttribute(ctor); - var typeTypeRef = module.ImportReference(typeof(Type)); - attribute.ConstructorArguments.Add(new CustomAttributeArgument( - module.ImportReference(typeof(Type[])), - types.Select(type => new CustomAttributeArgument(typeTypeRef, type)).ToArray())); - module.Assembly.CustomAttributes.Add(attribute); - } - private static AssemblyDefinition AssemblyDefinitionFor(ICompiledAssembly compiledAssembly) { var assemblyResolver = new PostProcessorAssemblyResolver(compiledAssembly); diff --git a/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorAssemblyResolver.cs b/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorAssemblyResolver.cs index 38c7606..1ef8de1 100644 --- a/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorAssemblyResolver.cs +++ b/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorAssemblyResolver.cs @@ -8,7 +8,7 @@ using Unity.CompilationPipeline.Common.ILPostProcessing; namespace ShrinkEventBus.CodeGen { - internal sealed class PostProcessorAssemblyResolver : IAssemblyResolver + public sealed class PostProcessorAssemblyResolver : IAssemblyResolver { private readonly string[] _references; private readonly Dictionary _cache = new(); diff --git a/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorReflectionImporter.cs b/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorReflectionImporter.cs index 957a46d..839c71f 100644 --- a/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorReflectionImporter.cs +++ b/Assets/Modules/ShrinkEventBus/CodeGen/Editor/PostProcessorReflectionImporter.cs @@ -6,7 +6,7 @@ using Mono.Cecil; namespace ShrinkEventBus.CodeGen { - internal sealed class PostProcessorReflectionImporter : DefaultReflectionImporter + public sealed class PostProcessorReflectionImporter : DefaultReflectionImporter { private const string CoreLibName = "System.Private.CoreLib"; private readonly AssemblyNameReference? _corlib; @@ -27,7 +27,7 @@ namespace ShrinkEventBus.CodeGen } } - internal sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider + public sealed class PostProcessorReflectionImporterProvider : IReflectionImporterProvider { public IReflectionImporter GetReflectionImporter(ModuleDefinition module) { diff --git a/Assets/Modules/ShrinkEventBus/Editor/EventBusViewerWindow.cs b/Assets/Modules/ShrinkEventBus/Editor/EventBusViewerWindow.cs index ea0d260..ff8b265 100644 --- a/Assets/Modules/ShrinkEventBus/Editor/EventBusViewerWindow.cs +++ b/Assets/Modules/ShrinkEventBus/Editor/EventBusViewerWindow.cs @@ -1,8 +1,13 @@ -#if UNITY_EDITOR +#nullable enable + using System; -using System.Collections; using System.Collections.Generic; -using System.Reflection; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; @@ -10,784 +15,892 @@ using UnityEngine.UIElements; namespace ShrinkEventBus.Editor { - public class EventBusViewerWindow : EditorWindow + public sealed class EventBusViewerWindow : EditorWindow { - private enum LiveLogFilterMode + private sealed class BusRow { - All, - HasListeners, - NoListeners + public string Key = string.Empty; + public string FilterKey = string.Empty; + public string Scheduler = string.Empty; + public string Mode = string.Empty; + public string Queue = string.Empty; + public string Overflow = string.Empty; + public int Subscribers; } - private sealed class HandlerDisplayData + private sealed class EventRow { - public object TargetInstance { get; set; } - public MonoBehaviour PingTarget { get; set; } - public string TargetName { get; set; } - public string MethodName { get; set; } - public object Priority { get; set; } - public string ExecType { get; set; } - public bool IsStaticHandler { get; set; } + public long Sequence; + public DateTime TimestampUtc; + public string Bus = string.Empty; + public string EventType = string.Empty; + public string ShortEventType = string.Empty; + public string Scheduler = string.Empty; + public string Mode = string.Empty; + public int ThreadId; + public double DurationMicroseconds; + public bool IsAsync; + public bool Accepted; + public bool Handled; + public bool Canceled; + public ShrinkPostFailure Failure; + + public string Status => Failure != ShrinkPostFailure.None + ? FormatFailure(Failure) + : Canceled + ? "已取消" + : Handled + ? "已处理" + : "无处理器"; } - private sealed class EventLogRecord - { - public DateTime Timestamp { get; set; } - public string TimestampText { get; set; } - public string EventName { get; set; } - public string SenderInfo { get; set; } - public bool HasListeners { get; set; } - public bool IsCancelable { get; set; } - public bool IsCanceled { get; set; } - public string Result { get; set; } - public string Phase { get; set; } - public List Parameters { get; set; } - public List Handlers { get; set; } - public string MatchText { get; set; } - } + private const int MaxEvents = 5000; + private const int MaxPendingEvents = 20000; + private static readonly Color SuccessColor = new(0.32f, 0.72f, 0.48f); + private static readonly Color WarningColor = new(0.94f, 0.68f, 0.24f); + private static readonly Color ErrorColor = new(0.95f, 0.36f, 0.34f); + private static readonly Color MutedColor = new(0.58f, 0.61f, 0.66f); - private sealed class EventLogGroup - { - public string EventName { get; set; } - public List Records { get; } = new(); - public EventLogRecord LatestRecord => Records[0]; - } + private readonly object _eventGate = new(); + private readonly Queue _pendingEvents = new(); + private readonly List _events = new(); + private readonly List _filteredEvents = new(); + private readonly List _buses = new(); - private ScrollView _subscribersScrollView; - private ScrollView _liveTrackerScrollView; - private VisualElement _subscribersTab; - private VisualElement _liveTrackerTab; - private Label _instanceCountLabel; - private Label _liveLogStatsLabel; - private string _searchString = ""; - private string _liveLogSearchString = ""; - private LiveLogFilterMode _liveLogFilterMode = LiveLogFilterMode.All; - private bool _collapseSameEventType = true; - private const int MaxLogCount = 100; - private const double LiveTrackerRefreshIntervalSeconds = 0.1d; - private readonly List _logRecords = new(); - private bool _isLiveTrackerDirty; - private double _nextLiveTrackerRefreshTime; + private ListView? _busList; + private ListView? _eventList; + private Label? _eventEmptyState; + private ToolbarSearchField? _search; + private ToolbarToggle? _captureToggle; + private ToolbarToggle? _problemOnlyToggle; + private ToolbarToggle? _autoScrollToggle; + private Label? _captureState; + private Label? _status; + private Label? _busDescription; + private Label? _busCountMetric; + private Label? _handlerCountMetric; + private Label? _capturedMetric; + private Label? _rateMetric; + private Label? _latencyMetric; + private Label? _problemMetric; + private Label? _detailTitle; + private Label? _detailTime; + private Label? _detailBus; + private Label? _detailExecution; + private Label? _detailThread; + private Label? _detailDuration; + private Label? _detailResult; + + private string _selectedBus = string.Empty; + private long _selectedEventSequence; + private long _nextSequence; + private long _droppedCaptureCount; + private double _nextRefresh; + private bool _captureRequested = true; + private bool _diagnosticsSubscribed; [MenuItem("ShrinkSDK/事件总线/事件查看器")] - public static void ShowWindow() + private static void Open() { - var window = GetWindow("EventBus 控制台"); - window.minSize = new Vector2(500, 600); + var window = GetWindow(); + window.titleContent = new GUIContent("事件总线 2.0"); + window.minSize = new Vector2(980f, 560f); window.Show(); } private void OnEnable() { - EventBus.OnEventTriggeredForEditor += RecordEventLog; - EditorApplication.update += OnEditorUpdate; + SetDiagnosticsSubscription(_captureRequested); + EditorApplication.update += Tick; } private void OnDisable() { - EventBus.OnEventTriggeredForEditor -= RecordEventLog; - EditorApplication.update -= OnEditorUpdate; + SetDiagnosticsSubscription(false); + EditorApplication.update -= Tick; } - private void CreateGUI() + public void CreateGUI() { - var root = rootVisualElement; - root.style.flexDirection = FlexDirection.Column; - root.Add(CreateHeader()); - root.Add(CreateTabBar()); + rootVisualElement.Clear(); + rootVisualElement.style.flexDirection = FlexDirection.Column; + rootVisualElement.style.backgroundColor = EditorGUIUtility.isProSkin + ? new Color(0.105f, 0.112f, 0.125f) + : new Color(0.88f, 0.89f, 0.91f); - var contentContainer = new VisualElement { style = { flexGrow = 1 } }; - _subscribersTab = CreateSubscribersTab(); - _liveTrackerTab = CreateLiveTrackerTab(); - _subscribersTab.style.display = DisplayStyle.Flex; - _liveTrackerTab.style.display = DisplayStyle.None; + rootVisualElement.Add(BuildToolbar()); + rootVisualElement.Add(BuildMetricsBand()); - contentContainer.Add(_subscribersTab); - contentContainer.Add(_liveTrackerTab); - root.Add(contentContainer); + var mainSplit = new TwoPaneSplitView(0, 330f, TwoPaneSplitViewOrientation.Horizontal); + mainSplit.style.flexGrow = 1f; + mainSplit.Add(BuildBusPane()); + mainSplit.Add(BuildEventWorkspace()); + rootVisualElement.Add(mainSplit); - RefreshSubscribersView(); + _status = new Label(); + _status.style.height = 24f; + _status.style.paddingLeft = 10f; + _status.style.paddingTop = 4f; + _status.style.borderTopWidth = 1f; + _status.style.borderTopColor = BorderColor(); + _status.style.color = MutedColor; + rootVisualElement.Add(_status); - root.schedule.Execute(() => - { - _instanceCountLabel.text = Application.isPlaying - ? $"活跃实例数: {EventBus.GetRegisteredInstanceCount()}" - : "游戏未运行"; - }).Every(500); + RefreshAll(); } - private VisualElement CreateHeader() - { - var header = new VisualElement - { - style = - { - paddingTop = 10, paddingBottom = 10, paddingLeft = 10, paddingRight = 10, - backgroundColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f, 1f)), borderBottomWidth = 1, - borderBottomColor = new StyleColor(Color.black) - } - }; - header.Add(new Label("Shrink Event Bus 控制台") - { style = { fontSize = 16, unityFontStyleAndWeight = FontStyle.Bold } }); - var debugToggle = new Toggle("启用 Debug 调试记录 (追踪实时事件)") - { value = EventBus.EnableDebugRecord, style = { marginTop = 5 } }; - debugToggle.RegisterValueChangedCallback(evt => EventBus.EnableDebugRecord = evt.newValue); - header.Add(debugToggle); - var infoRow = new VisualElement - { style = { flexDirection = FlexDirection.Row, marginTop = 5, justifyContent = Justify.SpaceBetween } }; - _instanceCountLabel = new Label("活跃实例数: 0") { style = { color = new StyleColor(Color.cyan) } }; - infoRow.Add(_instanceCountLabel); - header.Add(infoRow); - return header; - } - - private VisualElement CreateTabBar() + private VisualElement BuildToolbar() { var toolbar = new Toolbar(); - toolbar.Add(new ToolbarButton(() => SwitchTab(0)) - { text = "订阅者全览", style = { flexGrow = 1, unityTextAlign = TextAnchor.MiddleCenter } }); - toolbar.Add(new ToolbarButton(() => SwitchTab(1)) - { text = "实时触发日志", style = { flexGrow = 1, unityTextAlign = TextAnchor.MiddleCenter } }); + toolbar.style.height = 34f; + + var title = new Label("ShrinkEventBus"); + title.style.unityFontStyleAndWeight = FontStyle.Bold; + title.style.fontSize = 13f; + title.style.marginLeft = 4f; + title.style.marginRight = 8f; + toolbar.Add(title); + + _captureState = new Label(); + _captureState.style.minWidth = 48f; + _captureState.style.height = 18f; + _captureState.style.marginRight = 10f; + _captureState.style.paddingLeft = 6f; + _captureState.style.paddingRight = 6f; + _captureState.style.unityTextAlign = TextAnchor.MiddleCenter; + _captureState.style.unityFontStyleAndWeight = FontStyle.Bold; + _captureState.style.fontSize = 9f; + toolbar.Add(_captureState); + + _search = new ToolbarSearchField { tooltip = "按总线标识或事件类型筛选" }; + _search.style.width = 260f; + _search.RegisterValueChangedCallback(_ => ApplyFilter()); + toolbar.Add(_search); + + _problemOnlyToggle = new ToolbarToggle { text = "仅问题" }; + _problemOnlyToggle.tooltip = "只显示已取消、已拒绝或失败的发布"; + _problemOnlyToggle.RegisterValueChangedCallback(_ => ApplyFilter()); + toolbar.Add(_problemOnlyToggle); + + _autoScrollToggle = new ToolbarToggle { text = "跟随", value = true }; + _autoScrollToggle.tooltip = "自动保持最新事件可见"; + toolbar.Add(_autoScrollToggle); + + _captureToggle = new ToolbarToggle { text = "捕获", value = _captureRequested }; + _captureToggle.tooltip = "启用详细分发采样"; + _captureToggle.RegisterValueChangedCallback(evt => + { + _captureRequested = evt.newValue; + SetDiagnosticsSubscription(evt.newValue); + UpdateCaptureState(); + }); + toolbar.Add(_captureToggle); + + var spacer = new VisualElement(); + spacer.style.flexGrow = 1f; + toolbar.Add(spacer); + + toolbar.Add(new ToolbarButton(RefreshAll) { text = "刷新", tooltip = "刷新总线状态" }); + toolbar.Add(new ToolbarButton(ExportCsv) { text = "导出", tooltip = "将当前可见事件导出为 CSV" }); + toolbar.Add(new ToolbarButton(ClearEvents) { text = "清空", tooltip = "清空已捕获事件" }); + + UpdateCaptureState(); return toolbar; } - private VisualElement CreateSubscribersTab() + private VisualElement BuildMetricsBand() { - var container = new VisualElement { style = { flexGrow = 1 } }; - var toolbar = new Toolbar(); - var searchField = new ToolbarSearchField { style = { flexGrow = 1 } }; - searchField.RegisterValueChangedCallback(evt => - { - _searchString = evt.newValue; - RefreshSubscribersView(); - }); - toolbar.Add(searchField); - toolbar.Add(new ToolbarButton(RefreshSubscribersView) { text = "刷新视图" }); - container.Add(toolbar); - _subscribersScrollView = new ScrollView - { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5, paddingTop = 5 } }; - container.Add(_subscribersScrollView); - return container; + var band = new VisualElement(); + band.style.height = 58f; + band.style.flexDirection = FlexDirection.Row; + band.style.borderBottomWidth = 1f; + band.style.borderBottomColor = BorderColor(); + band.style.backgroundColor = PanelColor(); + + _busCountMetric = AddMetric(band, "总线"); + _handlerCountMetric = AddMetric(band, "处理器"); + _capturedMetric = AddMetric(band, "已捕获"); + _rateMetric = AddMetric(band, "事件 / 秒"); + _latencyMetric = AddMetric(band, "平均分发耗时"); + _problemMetric = AddMetric(band, "问题"); + return band; } - private VisualElement CreateLiveTrackerTab() + private static Label AddMetric(VisualElement parent, string caption) { - var container = new VisualElement { style = { flexGrow = 1 } }; - var toolbar = new Toolbar(); - var searchField = new ToolbarSearchField { style = { flexGrow = 1 } }; - searchField.RegisterValueChangedCallback(evt => - { - _liveLogSearchString = evt.newValue ?? string.Empty; - RequestLiveTrackerRefresh(true); - }); - toolbar.Add(searchField); + var block = new VisualElement(); + block.style.flexGrow = 1f; + block.style.flexBasis = 0f; + block.style.paddingLeft = 12f; + block.style.paddingTop = 6f; + block.style.borderRightWidth = 1f; + block.style.borderRightColor = BorderColor(); - var filterMenu = new ToolbarMenu { text = "全部事件" }; - filterMenu.menu.AppendAction("全部事件", _ => - { - _liveLogFilterMode = LiveLogFilterMode.All; - filterMenu.text = "全部事件"; - RequestLiveTrackerRefresh(true); - }); - filterMenu.menu.AppendAction("仅有监听者", _ => - { - _liveLogFilterMode = LiveLogFilterMode.HasListeners; - filterMenu.text = "仅有监听者"; - RequestLiveTrackerRefresh(true); - }); - filterMenu.menu.AppendAction("仅无监听者", _ => - { - _liveLogFilterMode = LiveLogFilterMode.NoListeners; - filterMenu.text = "仅无监听者"; - RequestLiveTrackerRefresh(true); - }); - toolbar.Add(filterMenu); + var captionLabel = new Label(caption); + captionLabel.style.fontSize = 9f; + captionLabel.style.color = MutedColor; + block.Add(captionLabel); - var collapseToggle = new ToolbarToggle + var value = new Label("0"); + value.style.fontSize = 17f; + value.style.unityFontStyleAndWeight = FontStyle.Bold; + value.style.marginTop = 1f; + block.Add(value); + parent.Add(block); + return value; + } + + private VisualElement BuildBusPane() + { + var pane = new VisualElement(); + pane.style.flexGrow = 1f; + pane.style.backgroundColor = PanelColor(); + pane.Add(BuildBusHeader()); + + _busList = new ListView(_buses, 30f, MakeBusRow, BindBusRow) { - text = "折叠同类事件", - value = _collapseSameEventType + selectionType = SelectionType.Single, + showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly, + virtualizationMethod = CollectionVirtualizationMethod.FixedHeight }; - collapseToggle.RegisterValueChangedCallback(evt => + _busList.style.flexGrow = 1f; + _busList.selectionChanged += selection => { - _collapseSameEventType = evt.newValue; - RequestLiveTrackerRefresh(true); - }); - toolbar.Add(collapseToggle); - toolbar.Add(new ToolbarButton(ClearLogs) { text = "清空日志" }); - container.Add(toolbar); - - _liveLogStatsLabel = new Label - { - style = - { - marginLeft = 8, - marginRight = 8, - marginTop = 4, - marginBottom = 4, - color = new StyleColor(Color.gray) - } - }; - container.Add(_liveLogStatsLabel); - - _liveTrackerScrollView = new ScrollView { style = { flexGrow = 1, paddingLeft = 5, paddingRight = 5 } }; - container.Add(_liveTrackerScrollView); - RequestLiveTrackerRefresh(true); - return container; - } - - private void SwitchTab(int index) - { - _subscribersTab.style.display = index == 0 ? DisplayStyle.Flex : DisplayStyle.None; - _liveTrackerTab.style.display = index == 1 ? DisplayStyle.Flex : DisplayStyle.None; - if (index == 0) - { - RefreshSubscribersView(); - return; - } - RequestLiveTrackerRefresh(true); - } - - private void OnEditorUpdate() - { - if (!_isLiveTrackerDirty) return; - if (_liveTrackerTab == null || _liveTrackerTab.style.display != DisplayStyle.Flex) return; - if (EditorApplication.timeSinceStartup < _nextLiveTrackerRefreshTime) return; - RefreshLiveTrackerView(); - } - - private void RequestLiveTrackerRefresh(bool immediate = false) - { - _isLiveTrackerDirty = true; - if (immediate) - { - _nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup; - if (_liveTrackerTab != null && _liveTrackerTab.style.display == DisplayStyle.Flex) - RefreshLiveTrackerView(); - } - } - - private void RefreshSubscribersView() - { - _subscribersScrollView.Clear(); - if (!Application.isPlaying) - { - _subscribersScrollView.Add(new HelpBox("请先运行游戏,系统初始化后即可查看内存中的事件树。", HelpBoxMessageType.Info)); - return; - } - - var snapshots = EventBus.GetAllSubscribersSnapshot(); - foreach (var entry in snapshots) - { - var eventType = entry.Key; - if (!string.IsNullOrEmpty(_searchString) && - !eventType.Name.ToLower().Contains(_searchString.ToLower())) continue; - - var handlers = entry.Value; - - var foldout = new Foldout - { - text = $"{eventType.Name} (挂载数: {handlers.Length})", - style = - { - backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.5f)), - marginTop = 2, - paddingBottom = 2, - borderBottomWidth = 1, - borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f)) - } - }; - - foreach (var h in handlers) foldout.Add(CreateHandlerRow(h)); - _subscribersScrollView.Add(foldout); - } - } - - private void RecordEventLog(EventBase evt, string eventName, string senderInfo, EventHandlerInfo[] handlers) - { - if (!EventBus.EnableDebugRecord) return; - _logRecords.Add(CreateEventLogRecord(evt, eventName, senderInfo, handlers)); - if (_logRecords.Count > MaxLogCount) - _logRecords.RemoveAt(0); - RequestLiveTrackerRefresh(); - } - - private EventLogRecord CreateEventLogRecord(EventBase evt, string eventName, string senderInfo, - EventHandlerInfo[] handlers) - { - var timestamp = DateTime.Now; - var hasListeners = handlers.Length > 0; - var paramsList = DumpEventParams(evt); - var handlerRows = new List(handlers.Length); - foreach (var handler in handlers) - handlerRows.Add(ExtractHandlerDisplayData(handler)); - - var isCancelable = (GetMemberValue(evt, "IsCancelable") as bool?) ?? false; - var isCanceled = (GetMemberValue(evt, "IsCanceled") as bool?) ?? false; - var resultStr = GetMemberValue(evt, "Result")?.ToString() ?? "DEFAULT"; - var phaseStr = GetMemberValue(evt, "Phase")?.ToString() ?? "NORMAL"; - - return new EventLogRecord - { - Timestamp = timestamp, - TimestampText = timestamp.ToString("HH:mm:ss.fff"), - EventName = eventName, - SenderInfo = senderInfo, - HasListeners = hasListeners, - IsCancelable = isCancelable, - IsCanceled = isCanceled, - Result = resultStr, - Phase = phaseStr, - Parameters = paramsList, - Handlers = handlerRows, - MatchText = BuildLogMatchText(eventName, senderInfo, paramsList) - }; - } - - private string BuildLogMatchText(string eventName, string senderInfo, List paramsList) - { - if (paramsList.Count == 0) - return $"{eventName}\n{senderInfo}"; - return $"{eventName}\n{senderInfo}\n{string.Join("\n", paramsList)}"; - } - - private void RefreshLiveTrackerView() - { - if (_liveTrackerScrollView == null) return; - _isLiveTrackerDirty = false; - _nextLiveTrackerRefreshTime = EditorApplication.timeSinceStartup + LiveTrackerRefreshIntervalSeconds; - - _liveTrackerScrollView.Clear(); - - var filteredRecords = new List(_logRecords.Count); - for (var i = _logRecords.Count - 1; i >= 0; i--) - { - var record = _logRecords[i]; - if (MatchesLiveLogFilter(record)) - filteredRecords.Add(record); - } - - if (_liveLogStatsLabel != null) - { - _liveLogStatsLabel.text = _collapseSameEventType - ? $"显示 {filteredRecords.Count} / {_logRecords.Count} 条,按事件类型折叠" - : $"显示 {filteredRecords.Count} / {_logRecords.Count} 条"; - } - - if (filteredRecords.Count == 0) - { - _liveTrackerScrollView.Add(new HelpBox("当前没有符合条件的实时事件日志。", HelpBoxMessageType.Info)); - return; - } - - if (_collapseSameEventType) - { - foreach (var group in BuildCollapsedGroups(filteredRecords)) - _liveTrackerScrollView.Add(CreateCollapsedGroupElement(group)); - return; - } - - foreach (var record in filteredRecords) - _liveTrackerScrollView.Add(CreateLogEntryElement(record)); - } - - private bool MatchesLiveLogFilter(EventLogRecord record) - { - switch (_liveLogFilterMode) - { - case LiveLogFilterMode.HasListeners: - if (!record.HasListeners) return false; - break; - case LiveLogFilterMode.NoListeners: - if (record.HasListeners) return false; - break; - } - - if (string.IsNullOrWhiteSpace(_liveLogSearchString)) - return true; - - return record.MatchText.IndexOf(_liveLogSearchString.Trim(), StringComparison.OrdinalIgnoreCase) >= 0; - } - - private List BuildCollapsedGroups(List records) - { - var groups = new List(); - var groupMap = new Dictionary(StringComparer.Ordinal); - - foreach (var record in records) - { - if (!groupMap.TryGetValue(record.EventName, out var group)) - { - group = new EventLogGroup { EventName = record.EventName }; - groupMap[record.EventName] = group; - groups.Add(group); - } - - group.Records.Add(record); - } - - return groups; - } - - private VisualElement CreateCollapsedGroupElement(EventLogGroup group) - { - var latest = group.LatestRecord; - var foldout = new Foldout - { - text = $"{group.EventName} × {group.Records.Count} 最近: {latest.TimestampText}", - value = group.Records.Count == 1, - style = - { - backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.4f)), - marginTop = 2, - paddingBottom = 2, - borderBottomWidth = 1, - borderBottomColor = new StyleColor(new Color(0.1f, 0.1f, 0.1f)) - } - }; - - var summary = new Label( - $"最近来源: {latest.SenderInfo} | 监听者: {(latest.HasListeners ? latest.Handlers.Count : 0)} | 已取消: {(latest.IsCanceled ? "是" : "否")}") - { - style = - { - marginLeft = 18, - marginBottom = 4, - color = new StyleColor(Color.gray) - } - }; - foldout.Add(summary); - if (foldout.value) - PopulateCollapsedGroupDetails(foldout, group, 1); - foldout.RegisterValueChangedCallback(evt => - { - if (evt.newValue) - { - PopulateCollapsedGroupDetails(foldout, group, 1); + var selected = selection.OfType().FirstOrDefault(); + if (selected == null) return; - } - ClearFoldoutChildren(foldout, 1); - }); - - return foldout; - } - - private void PopulateCollapsedGroupDetails(Foldout foldout, EventLogGroup group, int keepChildCount) - { - if (foldout.childCount > keepChildCount) return; - foreach (var record in group.Records) - { - var row = CreateLogEntryElement(record); - row.style.marginLeft = 12; - foldout.Add(row); - } - } - - private void ClearFoldoutChildren(Foldout foldout, int keepChildCount) - { - while (foldout.childCount > keepChildCount) - foldout.RemoveAt(foldout.childCount - 1); - } - - private VisualElement CreateLogEntryElement(EventLogRecord record) - { - var logEntry = new VisualElement - { - style = - { - flexDirection = FlexDirection.Column, - paddingBottom = 4, - paddingTop = 4, - borderBottomWidth = 1, - borderBottomColor = new StyleColor(new Color(0.15f, 0.15f, 0.15f)) - } + _selectedBus = selected.FilterKey; + UpdateBusDescription(selected); + ApplyFilter(); }; + pane.Add(_busList); - var headerRow = new VisualElement - { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; - headerRow.Add(new Label(record.TimestampText) - { style = { width = 80, color = new StyleColor(Color.gray) } }); - headerRow.Add(new Label(record.EventName) - { - style = { color = new StyleColor(new Color(1f, 0.84f, 0f)), unityFontStyleAndWeight = FontStyle.Bold } - }); + _busDescription = new Label(); + _busDescription.style.minHeight = 48f; + _busDescription.style.paddingLeft = 10f; + _busDescription.style.paddingRight = 8f; + _busDescription.style.paddingTop = 7f; + _busDescription.style.paddingBottom = 6f; + _busDescription.style.borderTopWidth = 1f; + _busDescription.style.borderTopColor = BorderColor(); + _busDescription.style.color = MutedColor; + _busDescription.style.whiteSpace = WhiteSpace.Normal; + pane.Add(_busDescription); + return pane; + } - var senderClassName = GetSenderClassName(record.SenderInfo); - var senderLabel = new Label($" [由 {record.SenderInfo} 触发]") + private VisualElement BuildEventWorkspace() + { + var split = new TwoPaneSplitView(1, 185f, TwoPaneSplitViewOrientation.Vertical); + split.style.flexGrow = 1f; + split.Add(BuildEventPane()); + split.Add(BuildDetailPane()); + return split; + } + + private VisualElement BuildEventPane() + { + var pane = new VisualElement(); + pane.style.flexGrow = 1f; + pane.Add(BuildEventHeader()); + + _eventList = new ListView(_filteredEvents, 28f, MakeEventRow, BindEventRow) { - style = - { - color = new StyleColor(new Color(0.5f, 0.8f, 1f)), - marginLeft = 5, - unityFontStyleAndWeight = FontStyle.Italic - } + selectionType = SelectionType.Single, + showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly, + virtualizationMethod = CollectionVirtualizationMethod.FixedHeight }; - - if (senderClassName != "未知来源" && senderClassName != "EventBus" && senderClassName != "Unknown") + _eventList.style.flexGrow = 1f; + _eventList.selectionChanged += selection => { - senderLabel.RegisterCallback(_ => - senderLabel.style.color = new StyleColor(Color.cyan)); - senderLabel.RegisterCallback(_ => - senderLabel.style.color = new StyleColor(new Color(0.5f, 0.8f, 1f))); - senderLabel.RegisterCallback(_ => OpenScriptByClassName(senderClassName)); - } - - headerRow.Add(senderLabel); - logEntry.Add(headerRow); - - var statusRow = new VisualElement - { style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } }; - statusRow.Add(new Label($"可取消: {(record.IsCancelable ? "是" : "否")}") - { style = { color = new StyleColor(Color.gray), marginRight = 15 } }); - - var canceledLabel = new Label($"已取消: {(record.IsCanceled ? "是" : "否")}") - { style = { marginRight = 15 } }; - canceledLabel.style.color = - record.IsCanceled ? new StyleColor(new Color(1f, 0.3f, 0.3f)) : new StyleColor(Color.gray); - statusRow.Add(canceledLabel); - - statusRow.Add(new Label($"Result: {record.Result}") - { style = { color = new StyleColor(new Color(0.8f, 0.6f, 0.8f)), marginRight = 15 } }); - statusRow.Add(new Label($"最后阶段: {record.Phase}") - { style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } }); - logEntry.Add(statusRow); - - var paramsContainer = new VisualElement - { style = { flexDirection = FlexDirection.Column, marginLeft = 80, marginTop = 2, marginBottom = 2 } }; - if (record.Parameters.Count == 0) - { - paramsContainer.Add(new Label("参数: { 无 }") - { style = { color = new StyleColor(new Color(0.5f, 0.5f, 0.5f)) } }); - } - else - { - paramsContainer.Add( - new Label("携带参数:") { style = { color = new StyleColor(new Color(0.6f, 0.8f, 1f)) } }); - foreach (var pStr in record.Parameters) - paramsContainer.Add(new Label($" • {pStr}") - { style = { color = new StyleColor(new Color(0.6f, 0.9f, 0.6f)), marginLeft = 10 } }); - } - - logEntry.Add(paramsContainer); - - if (record.HasListeners) - { - foreach (var handler in record.Handlers) - logEntry.Add(CreateHandlerRow(handler, 80)); - } - else - { - var noHandlerRow = new VisualElement - { style = { flexDirection = FlexDirection.Row, marginLeft = 80, marginTop = 2 } }; - noHandlerRow.Add(new Label("(无监听者响应)") { style = { color = new StyleColor(Color.gray) } }); - logEntry.Add(noHandlerRow); - } - - return logEntry; - } - - private string GetSenderClassName(string senderInfo) - { - if (string.IsNullOrWhiteSpace(senderInfo)) - return "未知来源"; - return senderInfo.Contains(".") ? senderInfo.Split('.')[0] : senderInfo; - } - - private VisualElement CreateHandlerRow(object h) - { - return CreateHandlerRow(ExtractHandlerDisplayData(h), 40); - } - - private VisualElement CreateHandlerRow(HandlerDisplayData handlerData, float marginLeft) - { - var targetInstance = handlerData.TargetInstance; - var targetName = handlerData.TargetName; - var methodName = handlerData.MethodName; - var priority = handlerData.Priority; - var execType = handlerData.ExecType; - - var row = new VisualElement - { - style = - { - flexDirection = FlexDirection.Row, marginTop = 2, marginBottom = 2, marginLeft = marginLeft, - alignItems = Align.Center - } + var selected = selection.OfType().FirstOrDefault(); + if (selected == null) + return; + _selectedEventSequence = selected.Sequence; + ShowEventDetails(selected); }; + pane.Add(_eventList); - row.Add(new Label($"[{priority}]") { style = { width = 75, color = new StyleColor(Color.gray) } }); - row.Add(new Label($"[{execType}]") - { style = { width = 50, color = new StyleColor(new Color(0.8f, 0.6f, 0.2f)) } }); + _eventEmptyState = new Label("暂无事件"); + _eventEmptyState.style.flexGrow = 1f; + _eventEmptyState.style.unityTextAlign = TextAnchor.MiddleCenter; + _eventEmptyState.style.color = MutedColor; + pane.Add(_eventEmptyState); + return pane; + } - var linkLabel = new Label($"{targetName}.{methodName}()") - { style = { color = new StyleColor(new Color(0.3f, 0.6f, 1f)) } }; - linkLabel.RegisterCallback(e => linkLabel.style.color = new StyleColor(Color.cyan)); - linkLabel.RegisterCallback(e => - linkLabel.style.color = new StyleColor(new Color(0.3f, 0.6f, 1f))); - linkLabel.RegisterCallback(e => OpenScriptByClassName(targetName)); - linkLabel.style.flexGrow = 1; - row.Add(linkLabel); + private VisualElement BuildDetailPane() + { + var pane = new VisualElement(); + pane.style.flexGrow = 1f; + pane.style.backgroundColor = PanelColor(); + pane.style.borderTopWidth = 1f; + pane.style.borderTopColor = BorderColor(); - if (handlerData.PingTarget) - { - var pingBtn = new Button(() => - { - EditorGUIUtility.PingObject(handlerData.PingTarget.gameObject); - Selection.activeGameObject = handlerData.PingTarget.gameObject; - }) { text = "Ping", style = { width = 50, height = 18 } }; - row.Add(pingBtn); - } - else if (handlerData.IsStaticHandler && methodName != "未知方法") - row.Add(new Label("(静态)") - { - style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight } - }); - else - row.Add(new Label("(无对象)") - { - style = { width = 50, color = new StyleColor(Color.gray), unityTextAlign = TextAnchor.MiddleRight } - }); + _detailTitle = new Label("未选择事件"); + _detailTitle.style.fontSize = 13f; + _detailTitle.style.unityFontStyleAndWeight = FontStyle.Bold; + _detailTitle.style.marginLeft = 12f; + _detailTitle.style.marginTop = 9f; + _detailTitle.style.marginBottom = 7f; + pane.Add(_detailTitle); + var grid = new VisualElement(); + grid.style.flexDirection = FlexDirection.Row; + grid.style.flexWrap = Wrap.Wrap; + grid.style.paddingLeft = 8f; + grid.style.paddingRight = 8f; + _detailTime = AddDetailField(grid, "时间"); + _detailBus = AddDetailField(grid, "总线"); + _detailExecution = AddDetailField(grid, "执行方式"); + _detailThread = AddDetailField(grid, "线程"); + _detailDuration = AddDetailField(grid, "分发耗时"); + _detailResult = AddDetailField(grid, "结果"); + pane.Add(grid); + return pane; + } + + private static Label AddDetailField(VisualElement grid, string caption) + { + var field = new VisualElement(); + field.style.width = Length.Percent(33.333f); + field.style.minWidth = 190f; + field.style.paddingLeft = 5f; + field.style.paddingRight = 5f; + field.style.paddingBottom = 8f; + + var captionLabel = new Label(caption); + captionLabel.style.fontSize = 9f; + captionLabel.style.color = MutedColor; + field.Add(captionLabel); + + var value = new Label("-"); + value.style.marginTop = 2f; + value.style.overflow = Overflow.Hidden; + value.style.whiteSpace = WhiteSpace.Normal; + field.Add(value); + grid.Add(field); + return value; + } + + private static VisualElement BuildBusHeader() + { + var header = BuildHeaderContainer("总线"); + var columns = BuildColumnRow(); + columns.Add(MakeHeaderCell("标识", 1f)); + columns.Add(MakeHeaderCell("调度器", 0f, 92f)); + columns.Add(MakeHeaderCell("处理器", 0f, 58f, TextAnchor.MiddleRight)); + header.Add(columns); + return header; + } + + private static VisualElement BuildEventHeader() + { + var header = BuildHeaderContainer("事件流"); + var columns = BuildColumnRow(); + columns.Add(MakeHeaderCell("时间", 0f, 92f)); + columns.Add(MakeHeaderCell("总线", 0f, 132f)); + columns.Add(MakeHeaderCell("事件", 1f)); + columns.Add(MakeHeaderCell("方式", 0f, 54f)); + columns.Add(MakeHeaderCell("线程", 0f, 56f, TextAnchor.MiddleRight)); + columns.Add(MakeHeaderCell("耗时", 0f, 82f, TextAnchor.MiddleRight)); + columns.Add(MakeHeaderCell("结果", 0f, 90f)); + header.Add(columns); + return header; + } + + private static VisualElement BuildHeaderContainer(string title) + { + var header = new VisualElement(); + header.style.height = 52f; + header.style.borderBottomWidth = 1f; + header.style.borderBottomColor = BorderColor(); + header.style.backgroundColor = HeaderColor(); + var titleLabel = new Label(title); + titleLabel.style.height = 27f; + titleLabel.style.paddingLeft = 10f; + titleLabel.style.paddingTop = 7f; + titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold; + titleLabel.style.fontSize = 10f; + header.Add(titleLabel); + return header; + } + + private static VisualElement BuildColumnRow() + { + var row = new VisualElement(); + row.style.height = 24f; + row.style.flexDirection = FlexDirection.Row; + row.style.alignItems = Align.Center; return row; } - private HandlerDisplayData ExtractHandlerDisplayData(object handlerObj) + private static Label MakeHeaderCell(string text, float grow, float width = 0f, + TextAnchor alignment = TextAnchor.MiddleLeft) { - var priority = GetMemberValue(handlerObj, "Priority") ?? "NORMAL"; - var debugInfo = GetMemberValue(handlerObj, "DebugInfo") as string ?? "未知方法"; + var label = MakeCell(string.Empty, grow, width, alignment); + label.text = text; + label.style.fontSize = 9f; + label.style.color = MutedColor; + return label; + } - var execType = "Sync"; - if (debugInfo.Contains("(UniTask)") || debugInfo.Contains("Async")) execType = "Async"; - else if (debugInfo.Contains("(Task->UniTask)") || debugInfo.Contains("(Task)")) execType = "Task"; + private static VisualElement MakeBusRow() + { + var row = new VisualElement { name = "bus-row" }; + row.style.flexDirection = FlexDirection.Row; + row.style.alignItems = Align.Center; + row.Add(MakeCell("key", 1f)); + row.Add(MakeCell("scheduler", 0f, 92f)); + row.Add(MakeCell("handlers", 0f, 58f, TextAnchor.MiddleRight)); + return row; + } - object targetInstance = null; - var targetName = "未知类"; - var methodName = debugInfo; + private void BindBusRow(VisualElement element, int index) + { + if (index < 0 || index >= _buses.Count) + return; + var item = _buses[index]; + element.Q