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.
This commit is contained in:
+4
-2
@@ -11,8 +11,10 @@ namespace ExampleGreedMod
|
||||
{
|
||||
public override void OnRegisterContent(ShrinkModContext context)
|
||||
{
|
||||
context.GetRegistry<IReplacedPersonMod>(ReplacedPersonModBridge.RegistryName)
|
||||
.Register(context.ModInfo.ModId, context.ModInfo.ModId, new ExampleGreedRules());
|
||||
context.RegisterContent(
|
||||
ReplacedPersonModBridge.RegistryName,
|
||||
"rules",
|
||||
new ExampleGreedRules());
|
||||
}
|
||||
|
||||
public override void OnReady(ShrinkModContext context)
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>();
|
||||
public string[] EndCardIds { get; set; } = System.Array.Empty<string>();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -168,12 +168,12 @@ namespace ShrinkApp
|
||||
public ShrinkAppHostingMode hostingMode = ShrinkAppHostingMode.ClassicHost;
|
||||
}
|
||||
|
||||
public sealed class ShrinkAppStartedEvent : EventBase
|
||||
public sealed class ShrinkAppStartedEvent : IShrinkEvent
|
||||
{
|
||||
public IReadOnlyList<string> ModuleIds { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed class ShrinkAppStartFailedEvent : EventBase
|
||||
public sealed class ShrinkAppStartFailedEvent : IShrinkEvent
|
||||
{
|
||||
public string ErrorMessage { get; set; } = string.Empty;
|
||||
public string FailedModuleId { get; set; } = string.Empty;
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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<string, ServiceRegistration> RegisteredByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly Dictionary<ShrinkCommandService, ServiceRegistration> 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<ShrinkCommandExecuteRequestEvent>(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<string, string>(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,
|
||||
|
||||
+8
-5
@@ -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<string, string> 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;
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -19,6 +19,15 @@ namespace ShrinkContext.AppAdapter.Tests
|
||||
/// </summary>
|
||||
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<ShrinkAppStartedEvent>(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]
|
||||
|
||||
@@ -7,48 +7,25 @@ using ShrinkEventBus;
|
||||
namespace ShrinkContext.EventBusAdapter
|
||||
{
|
||||
/// <summary>
|
||||
/// 把 ShrinkEventBus 的订阅/注册包装为可逆效应:
|
||||
/// <c>IShrinkEventSubscription.Dispose</c> 天然就是订阅的逆操作——
|
||||
/// 组件在 apply 里订阅,停用时随上下文自动退订,无需手写卸载路径。
|
||||
/// 把生成的 ShrinkEventBus 绑定包装为可逆效应。
|
||||
/// </summary>
|
||||
public static class ShrinkEventBusEffects
|
||||
{
|
||||
/// <summary>订阅事件即效应:ctx 卸载或手动 Dispose 句柄时自动退订。</summary>
|
||||
public static ShrinkEffectHandle EffectSubscribe<TEvent>(
|
||||
this ShrinkCtx ctx,
|
||||
Action<TEvent> 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;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实例对象整体注册即效应:扫描对象上的 [EventSubscribe] 方法(含未织入的非 MonoBehaviour 类),
|
||||
/// 逆操作为 Unregister。
|
||||
/// 激活生成的 [ShrinkSubscribe] 绑定,并在 Context 回滚时统一释放。
|
||||
/// </summary>
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<int>();
|
||||
_ctx.EffectSubscribe<PingEvent>(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<int>();
|
||||
var second = new List<int>();
|
||||
_ctx.EffectSubscribe<PingEvent>(e => first.Add(e.Value));
|
||||
var handle = _ctx.EffectSubscribe<PingEvent>(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<int>();
|
||||
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<int> _values;
|
||||
|
||||
public InstanceSubscriber(List<int> values)
|
||||
{
|
||||
_values = values;
|
||||
}
|
||||
|
||||
public int LastValue { get; private set; }
|
||||
|
||||
[EventSubscribe]
|
||||
[ShrinkSubscribe]
|
||||
public void OnPing(PingEvent e)
|
||||
{
|
||||
LastValue = e.Value;
|
||||
_values.Add(e.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>() => (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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<float>());
|
||||
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<SaveCompletedEvent>(OnSave, EventPriority.LOWEST);
|
||||
EventBus.RegisterEvent<LoadCompletedEvent>(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<XxxEventArgs>)
|
||||
└─ ShrinkDataSaverEventBusComponent / 显式 Register
|
||||
└─ EventBus.TriggerEvent(new XxxEvent { ... })
|
||||
└─ ShrinkEventBus 分发到所有订阅者
|
||||
```
|
||||
|
||||
桥接器是单向的:DataSaver → EventBus。业务代码只需订阅 EventBus 事件,无需直接引用 ShrinkDataSaver 的原生事件。
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- **生命周期**:ContextLoader 组合根负责接桥;Standalone 必须在 DataSaver 初始化后显式 `Register()`,并在退出时 `Unregister()`。
|
||||
- **不影响原生事件**:桥接是附加行为,ShrinkDataSaver 的原生 C# 事件仍然正常触发,两种订阅方式可并存。
|
||||
- **SettingsChangedEvent.Get\<T\>()**:提供泛型辅助方法获取强类型值,内部使用 `Convert.ChangeType`。
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](LICENSE)
|
||||
`SettingsChangedEvent.Get<T>()` 内部使用 `Convert.ChangeType`,只应在设置值确实可转换时调用。
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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<TResult>`。
|
||||
|
||||
## [1.3.0] - 2026-06-12
|
||||
|
||||
### Breaking
|
||||
|
||||
@@ -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<TypeDefinition> GetAllTypes(IEnumerable<TypeDefinition> 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<TypeDefinition> 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 == "<Module>");
|
||||
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<Instruction>
|
||||
{
|
||||
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<Instruction>
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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<string, AssemblyDefinition> _cache = new();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,629 +1,171 @@
|
||||
# ShrinkEventBus
|
||||
# ShrinkEventBus 2.0
|
||||
|
||||
一个为 Unity C# 项目设计的高性能、类型安全事件总线系统。支持优先级调度、编译期自动注册、同步/异步混合处理,以及完整的 MonoBehaviour 生命周期管理。
|
||||
ShrinkEventBus 2.0 是面向 Unity、普通 C# 宿主和 Entities/Burst 生产端的统一事件总线。运行时只有一种事件模型、一个订阅入口和一组发布 API;不同 Bus 只表达生命周期、所有权与调度策略。
|
||||
|
||||
## ✨ 特性概览
|
||||
|
||||
| 特性 | 说明 |
|
||||
|------|------|
|
||||
| 🔒 **类型安全** | 基于泛型的强类型事件,编译期检查,无装箱开销 |
|
||||
| ⚡ **高性能热路径** | 注册期预编译 invoker(无反射调用、无装箱),派发走缓存快照数组,监听者快照零拷贝挂接 |
|
||||
| 🤖 **零侵入自动注册** | 标记 `[EventBusSubscriber]` 即可,ILPostProcessor 编译期自动织入注册与反注册逻辑,动态创建的对象也无需手写任何代码 |
|
||||
| 🧾 **静态订阅清单** | 静态 `[EventBusSubscriber]` 现可通过编译期注册表收口,避免默认总线启动时全域扫描所有类型 |
|
||||
| 🎯 **双重优先级** | 支持枚举优先级与数字优先级组合,精确控制执行顺序 |
|
||||
| 🔄 **同步 & 异步** | 统一支持 `Action`(同步)与 `UniTask`(异步)两种 handler 形式 |
|
||||
| 🧵 **线程安全** | 注册/注销操作全程加锁保护 |
|
||||
| 📦 **对象池** | 内置 `EventPool<T>`,高频事件零 GC |
|
||||
| 🔍 **调试友好** | Editor 事件查看器实时追踪订阅者与触发日志 |
|
||||
| 🧩 **可实例化总线** | 除默认静态 `EventBus` 外,也可以用 Builder 创建独立 bus,并按需要配置异常策略、事件类型约束、分 phase 分发 |
|
||||
| 🌳 **父事件监听** | 监听父事件类型时,子事件触发也会命中父事件监听器,便于做 Pre/Post 家族事件和统一监控 |
|
||||
|
||||
## 👓 Benchmark
|
||||
|
||||
[Benchmark结果](Benchmark.txt)
|
||||
|
||||
运行时仓库里自带 `EventBusBenchmark` 组件,当前会分别覆盖这些场景:
|
||||
|
||||
- 无订阅者 / 单订阅者 / 多订阅者同步触发
|
||||
- 父事件监听子事件
|
||||
- 按 `EventPriority` 分 phase 分发
|
||||
- 单订阅者异步触发
|
||||
- 手工 delegate 注册 / 注销
|
||||
- `object / Type / MethodInfo` 扫描注册 / 注销
|
||||
- `EventPool<T>` 与 `new`
|
||||
- 已取消事件跳过
|
||||
|
||||
如果你在评估这次 `IShrinkEventBus`、继承监听和严格注册带来的成本变化,优先看这个组件的输出,而不是只看 `Benchmark.txt` 里的旧样本。
|
||||
|
||||
## 📦 依赖
|
||||
|
||||
- Unity 2022.3+
|
||||
- [UniTask](https://github.com/Cysharp/UniTask) `2.x`
|
||||
|
||||
## ⚙️ 安装
|
||||
|
||||
在项目的 `Packages/manifest.json` 中添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
|
||||
"com.cneicy.shrink-eventbus": "https://github.com/cneicy/ShrinkEventBus.git"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或通过 Package Manager → `+` → `Add package from git URL` 输入:
|
||||
|
||||
```
|
||||
https://github.com/cneicy/ShrinkEventBus.git
|
||||
```
|
||||
|
||||
> ⚠️ **自动织入依赖说明**:当前工作区同时支持两层织入。若当前程序集命中共享管线 `ShrinkShared.CodeGen` 的覆盖范围(例如同时引用 `ShrinkCommand.Runtime` / `ShrinkNetwork.Runtime` / `ShrinkApp.Core.Runtime`),则优先由共享管线处理;其余只引用 `ShrinkEventBus.Runtime` 的程序集由模块内 `CodeGen/` 本地 ILPostProcessor 兜底。独立安装本包但未带上 `CodeGen/` 或共享管线时,没有编译期织入,请改用手动接入:MonoBehaviour 在 `Awake`/`OnDestroy` 中调用 `EventBus.AutoRegister(this)` / `EventBus.UnregisterInstance(this)`,或使用 `SubscribeEvent` 句柄。
|
||||
|
||||
## 🚀 快速上手
|
||||
|
||||
### 第一步:定义事件
|
||||
|
||||
所有事件必须继承 `EventBase`,通过 Attribute 声明附加能力:
|
||||
## 核心契约
|
||||
|
||||
```csharp
|
||||
// 普通事件
|
||||
public class PlayerDiedEvent : EventBase
|
||||
public interface IShrinkEvent { }
|
||||
|
||||
public interface IShrinkCancelableEvent : IShrinkEvent
|
||||
{
|
||||
public int PlayerId { get; set; }
|
||||
public string Cause { get; set; }
|
||||
bool IsCanceled { get; }
|
||||
void SetCanceled(bool value);
|
||||
}
|
||||
|
||||
// 可取消事件
|
||||
[Cancelable]
|
||||
public class PlayerMoveEvent : EventBase
|
||||
public interface IShrinkResultEvent<TResult> : IShrinkEvent
|
||||
{
|
||||
public Vector3 OldPosition { get; set; }
|
||||
public Vector3 NewPosition { get; set; }
|
||||
}
|
||||
|
||||
// 有返回结果的事件
|
||||
[HasResult]
|
||||
public class ItemPickupEvent : EventBase
|
||||
{
|
||||
public string ItemId { get; set; }
|
||||
public GameObject Picker { get; set; }
|
||||
TResult Result { get; }
|
||||
void SetResult(TResult result);
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:订阅事件
|
||||
事件可以是 class、struct 或 unmanaged struct,不需要继承 SDK 基类。
|
||||
|
||||
在 MonoBehaviour 上标记 `[EventBusSubscriber]`,用 `[EventSubscribe]` 标记处理方法。
|
||||
|
||||
**无论是场景初始时存在的对象,还是运行时动态 `Instantiate` 的对象,都会在 `Awake` 时自动完成注册,销毁时自动清理,无需手写任何注册代码。**
|
||||
## 多 Bus
|
||||
|
||||
```csharp
|
||||
[EventBusSubscriber]
|
||||
public class UIManager : MonoBehaviour
|
||||
var gameBus = EventBus.GetOrCreateBus(
|
||||
ShrinkBusKey.Game,
|
||||
ShrinkBusOptions.MainThread());
|
||||
|
||||
var modBus = EventBus.CreateBus(
|
||||
ShrinkBusKey.Mod("com.example.mod"),
|
||||
ShrinkBusOptions.DedicatedThread());
|
||||
|
||||
var workerBus = EventBus.CreateBus(
|
||||
new ShrinkBusKey("worker", "pathfinding"),
|
||||
ShrinkBusOptions.TaskPool(maxConcurrency: 4));
|
||||
```
|
||||
|
||||
标准 Key 包括 `Game`、`Server`、`Scene(id)`、`Mod(id)` 和 `World(id)`。调度器在 Bus 创建时确定,之后不可修改:
|
||||
|
||||
| Scheduler | 语义 |
|
||||
|---|---|
|
||||
| `Inline` | 在发布线程立即执行 |
|
||||
| `MainThread` | Unity PlayerLoop 主线程;已在主线程时直调 |
|
||||
| `DedicatedThread` | 有界队列、专属线程、Ordered 串行 |
|
||||
| `TaskPool` | 有界队列、显式最大并发;Parallel 忽略优先级顺序 |
|
||||
|
||||
`Post` 表示投递:同线程同步完成,跨线程入队后返回。`PostAsync` 等待全部 handler 完成,并传播取消与异常。需要结果或最终取消状态时使用 `PostAsync`。
|
||||
|
||||
## 唯一订阅入口
|
||||
|
||||
```csharp
|
||||
[ShrinkEventSubscriber(
|
||||
OwnerId = "com.example.mod",
|
||||
DefaultBus = "mod:com.example.mod")]
|
||||
public sealed class PlayerHandlers
|
||||
{
|
||||
// 同步处理
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnPlayerDied(PlayerDiedEvent evt)
|
||||
[ShrinkSubscribe(Priority = ShrinkEventPriority.High)]
|
||||
private void OnJoined(PlayerJoinedEvent value)
|
||||
{
|
||||
ShowDeathScreen(evt.PlayerId);
|
||||
}
|
||||
|
||||
// 异步处理(UniTask)
|
||||
[EventSubscribe(EventPriority.HIGH)]
|
||||
private async UniTask OnItemPickup(ItemPickupEvent evt)
|
||||
[ShrinkSubscribe]
|
||||
private async UniTask OnLoaded(
|
||||
PlayerLoadedEvent value,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await PlayPickupAnimation(evt.ItemId);
|
||||
await UniTask.Yield(cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第三步:触发事件
|
||||
Bus 解析顺序:方法 `Bus`、类型 `DefaultBus`、`Attach` 传入默认 Bus、否则 `game`。
|
||||
|
||||
普通对象由宿主管理生命周期:
|
||||
|
||||
```csharp
|
||||
// 同步触发
|
||||
EventBus.TriggerEvent(new PlayerDiedEvent { PlayerId = 1, Cause = "Fall" });
|
||||
using var binding = EventBus.Attach(new PlayerHandlers());
|
||||
```
|
||||
|
||||
// 异步触发(顺序等待每个 handler)
|
||||
await EventBus.TriggerEventAsync(new PlayerMoveEvent
|
||||
MonoBehaviour 不需要继承 SDK 基类。可自行在 `OnEnable/OnDisable` 中 Attach/Dispose,也可以在 GameObject 上添加 `ShrinkMonoEventScope`,由它统一绑定同对象或子层级中的生成 subscriber。
|
||||
|
||||
静态类型同样只使用特性:
|
||||
|
||||
```csharp
|
||||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||
public static class GlobalHandlers
|
||||
{
|
||||
OldPosition = transform.position,
|
||||
NewPosition = targetPos
|
||||
});
|
||||
|
||||
// 使用对象池(高频场景推荐)
|
||||
using var evt = EventPool<PlayerDiedEvent>.Get();
|
||||
evt.PlayerId = 1;
|
||||
EventBus.TriggerEvent(evt);
|
||||
// using 块结束时自动归还到池中
|
||||
```
|
||||
|
||||
## 🖼️ 追踪图形化
|
||||
|
||||
菜单栏 → `ShrinkSDK` → `事件总线` → `事件查看器`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
实时触发日志页现已支持关键词过滤、按“有监听者 / 无监听者”筛选,以及“折叠同类事件”聚合查看,适合排查高频事件刷屏场景。
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 自动注册机制
|
||||
|
||||
ShrinkEventBus 通过 **ILPostProcessor** 在编译期自动处理完整的生命周期管理。当 Unity 编译代码时,所有标记了 `[EventBusSubscriber]` 且自身或基类链上存在实例 `[EventSubscribe]` 方法的 MonoBehaviour 子类会被自动识别,并在其 `Awake` 和 `OnDestroy` 方法中分别织入注册与反注册逻辑(没有任何实例订阅方法的类型会被跳过,不织入也不报错)。
|
||||
|
||||
当前织入策略如下:
|
||||
|
||||
- 命中共享管线 `ShrinkShared.CodeGen` 覆盖范围的程序集,优先由共享管线处理。
|
||||
- 其余只引用 `ShrinkEventBus.Runtime` 的程序集,由模块内 `CodeGen/Editor/EventBusILPostProcessor.cs` 本地处理。
|
||||
- 因此 ShrinkSDK 工作区内的统一 CodeGen 与独立 `ShrinkEventBus` 业务程序集可以同时兼容,且不会双重织入。
|
||||
|
||||
织入规则如下:
|
||||
|
||||
- 类**自身已有** `Awake`/`OnDestroy`:在方法头部插入,用户自己负责 `base` 调用
|
||||
- 类**没有**,但**基类有虚方法**:生成 `protected override` 并自动调用 `base` 方法,`Awake` 顺序为 `base.Awake() → AutoRegister`,`OnDestroy` 顺序为 `UnregisterInstance → base.OnDestroy()`
|
||||
- 类**没有**,基类**也没有**:生成私有方法并插入
|
||||
|
||||
这意味着:
|
||||
|
||||
- 场景初始加载的对象 → `Awake` 执行时自动注册
|
||||
- 运行时 `Instantiate` 的对象 → `Awake` 执行时自动注册
|
||||
- GameObject 销毁时 → `OnDestroy` 执行时自动反注册,无内存泄漏
|
||||
|
||||
**整个过程对业务代码完全透明,类里不需要写任何注册相关的代码。**
|
||||
|
||||
### 实例化总线与 Builder
|
||||
|
||||
默认情况下,项目继续使用全局静态门面 `EventBus`。如果你需要更清晰的模块边界,也可以创建独立 bus:
|
||||
|
||||
```csharp
|
||||
var gameplayBus = EventBus.CreateBus(builder => builder
|
||||
.AllowPerPhaseDispatch()
|
||||
.SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndThrow));
|
||||
|
||||
gameplayBus.Register(new GameplaySubscribers());
|
||||
gameplayBus.TriggerEvent(new PlayerDiedEvent { PlayerId = 1, Cause = "Fall" });
|
||||
```
|
||||
|
||||
当前 Builder 支持的重点配置:
|
||||
|
||||
- `SetExceptionHandlingMode(...)`
|
||||
- `SetExceptionHandler(...)`
|
||||
- `AllowPerPhaseDispatch()`
|
||||
- `CheckTypesOnDispatch()`
|
||||
- `MarkerInterface<TMarker>()`
|
||||
- `ClassChecker(...)`
|
||||
- `StartShutdown()`
|
||||
|
||||
如果你只是想继续沿用旧习惯,直接用静态 `EventBus` 即可;它内部就是一个默认的 `IShrinkEventBus` 实例。
|
||||
|
||||
### 手动注册的新边界
|
||||
|
||||
除了 `AutoRegister(this)` / `[EventBusSubscriber]` 这条 Unity 友好的自动接入路径,现在也支持更显式的手动注册:
|
||||
|
||||
```csharp
|
||||
// 扫描实例上的 [EventSubscribe] 方法
|
||||
EventBus.Register(mySubscriberInstance);
|
||||
|
||||
// 扫描某个类型上的 static [EventSubscribe] 方法
|
||||
EventBus.Register(typeof(GlobalEventHooks));
|
||||
|
||||
// 只注册某一个 static [EventSubscribe] 方法
|
||||
EventBus.Register(typeof(GlobalEventHooks).GetMethod("OnPlayerDied",
|
||||
BindingFlags.Static | BindingFlags.NonPublic));
|
||||
```
|
||||
|
||||
和旧版本相比,手动注册现在会更严格:
|
||||
|
||||
- 方法必须带 `[EventSubscribe]`
|
||||
- 只能有一个参数
|
||||
- 参数必须继承 `EventBase`
|
||||
- 返回值只能是 `void` 或 `UniTask`
|
||||
- 实例注册只接受实例方法,类型/方法注册只接受静态方法
|
||||
|
||||
这样做的目的是把“为什么没触发”尽量提前到注册阶段暴露,而不是静默吞掉。
|
||||
|
||||
### 优先级系统
|
||||
|
||||
`EventPriority` 枚举定义了六个优先级档位,数值越小越先执行:
|
||||
|
||||
```
|
||||
HIGHEST(0) → HIGH(1) → NORMAL(2) → LOW(3) → LOWEST(4) → MONITOR(5)
|
||||
```
|
||||
|
||||
同一优先级档位内,可用数字优先级进一步细排(数字越大越先执行):
|
||||
|
||||
```csharp
|
||||
// 枚举优先级
|
||||
[EventSubscribe(EventPriority.HIGH)]
|
||||
private void Handler(SomeEvent evt) { }
|
||||
|
||||
// 数字优先级(自动映射到枚举档位;手动注册时必须显式传入数字)
|
||||
EventBus.RegisterEvent<SomeEvent>(Handler, priority: 75); // 映射为 HIGH
|
||||
|
||||
// 手动注册时混合使用
|
||||
EventBus.RegisterEvent<SomeEvent>(Handler, EventPriority.HIGH, receiveCanceled: false);
|
||||
```
|
||||
|
||||
数字到枚举的映射规则:
|
||||
|
||||
| 数字范围 | 枚举档位 |
|
||||
|---------|---------|
|
||||
| ≥ 100 | HIGHEST |
|
||||
| ≥ 50 | HIGH |
|
||||
| ≥ 0 | NORMAL |
|
||||
| ≥ -50 | LOW |
|
||||
| < -50 | LOWEST |
|
||||
|
||||
> 1.3.0 起:数字 `0` 映射到 `NORMAL`(与枚举重载默认值一致);int 重载不再提供默认值,不带优先级的 `RegisterEvent(handler)` 调用唯一解析到枚举重载(NORMAL)。
|
||||
|
||||
**推荐的优先级分工:**
|
||||
|
||||
```
|
||||
HIGHEST — 权限校验、合法性检查
|
||||
HIGH — 核心业务逻辑、数值计算
|
||||
NORMAL — 默认行为、状态变更
|
||||
LOW — UI 更新、音效、特效
|
||||
LOWEST — 收尾清理
|
||||
MONITOR — 日志、统计、监控(通常配合 receiveCanceled: true)
|
||||
```
|
||||
|
||||
如果你创建的 bus 开启了 `AllowPerPhaseDispatch()`,也可以只分发某一个 phase:
|
||||
|
||||
```csharp
|
||||
gameplayBus.TriggerEvent(EventPriority.HIGH, evt);
|
||||
await gameplayBus.TriggerEventAsync(EventPriority.MONITOR, evt);
|
||||
```
|
||||
|
||||
这个模式主要适合做框架级流水线控制;普通业务仍推荐直接走完整分发。
|
||||
|
||||
### 父事件监听
|
||||
|
||||
现在监听父事件时,子事件触发也会命中父事件监听器:
|
||||
|
||||
```csharp
|
||||
public class DamageEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CriticalDamageEvent : DamageEvent
|
||||
{
|
||||
public bool IsCritical { get; set; }
|
||||
}
|
||||
|
||||
EventBus.RegisterEvent<DamageEvent>(OnAnyDamage, EventPriority.MONITOR, receiveCanceled: true);
|
||||
EventBus.TriggerEvent(new CriticalDamageEvent { Value = 42, IsCritical = true });
|
||||
```
|
||||
|
||||
这很适合做统一日志、统一权限检查、事件族级别的监控和桥接。
|
||||
|
||||
### 事件取消与结果
|
||||
|
||||
```csharp
|
||||
// 取消事件(需标记 [Cancelable])
|
||||
[EventSubscribe(EventPriority.HIGHEST)]
|
||||
private void ValidateMove(PlayerMoveEvent evt)
|
||||
{
|
||||
if (!IsValidPosition(evt.NewPosition))
|
||||
evt.SetCanceled(true); // 后续未设置 receiveCanceled: true 的 handler 将跳过
|
||||
}
|
||||
|
||||
// 监控处理器可以接收已取消的事件
|
||||
[EventSubscribe(EventPriority.MONITOR, receiveCanceled: true)]
|
||||
private void LogMove(PlayerMoveEvent evt)
|
||||
{
|
||||
Debug.Log($"移动 {(evt.IsCanceled ? "被取消" : "成功")}");
|
||||
}
|
||||
|
||||
// 触发方检查取消状态
|
||||
var moveEvent = new PlayerMoveEvent { ... };
|
||||
await EventBus.TriggerEventAsync(moveEvent);
|
||||
if (!moveEvent.IsCanceled)
|
||||
transform.position = moveEvent.NewPosition;
|
||||
```
|
||||
|
||||
```csharp
|
||||
// 设置结果(需标记 [HasResult])
|
||||
[EventSubscribe(EventPriority.HIGH)]
|
||||
private void CheckPermission(ItemPickupEvent evt)
|
||||
{
|
||||
evt.SetResult(player.HasSpace ? EventResult.ALLOW : EventResult.DENY);
|
||||
}
|
||||
|
||||
// 触发方读取结果
|
||||
var pickupEvent = new ItemPickupEvent { ... };
|
||||
EventBus.TriggerEvent(pickupEvent);
|
||||
bool success = pickupEvent.Result switch
|
||||
{
|
||||
EventResult.ALLOW => true,
|
||||
EventResult.DENY => false,
|
||||
EventResult.DEFAULT => DefaultPickupLogic()
|
||||
};
|
||||
```
|
||||
|
||||
### 注册方式对比
|
||||
|
||||
| 方式 | 适用场景 | 自动反注册 |
|
||||
|------|---------|-----------|
|
||||
| `[EventBusSubscriber]` + `[EventSubscribe]` | MonoBehaviour(推荐) | ✅ ILP 织入 OnDestroy,随 GameObject 销毁自动清理 |
|
||||
| `EventBus.SubscribeEvent(...)` 手动订阅 | 非 MonoBehaviour 类、Lambda | ✅ `Dispose()` 即可精准清理 |
|
||||
| `EventBus.RegisterEvent(...)` 手动注册 | 兼容旧代码 | ❌ 需手动调用 `UnregisterEvent` |
|
||||
| `EventBus.AutoRegister(this)` | 特殊场景下手动触发 | ❌ 需手动调用 `UnregisterInstance` |
|
||||
|
||||
**手动注册示例(非 MonoBehaviour):**
|
||||
|
||||
```csharp
|
||||
public class InventorySystem : IDisposable
|
||||
{
|
||||
private readonly IShrinkEventSubscription _itemPickupSubscription;
|
||||
|
||||
public InventorySystem()
|
||||
{
|
||||
_itemPickupSubscription = EventBus.SubscribeEvent<ItemPickupEvent>(OnItemPickup, EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
private void OnItemPickup(ItemPickupEvent evt) { /* ... */ }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_itemPickupSubscription.Dispose();
|
||||
}
|
||||
[ShrinkSubscribe]
|
||||
private static void OnStartup(GameStartupEvent value) { }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
Unity ILPostProcessor 会生成直接调用桥和静态模块初始化注册;纯 .NET incremental generator 要求 subscriber 是 `partial`,并生成 ValueTask handler 绑定。生产路径不枚举程序集和方法,不使用 `MethodInfo.Invoke`、`DynamicInvoke` 或 `object[]` 参数。
|
||||
|
||||
## 🔧 API 参考
|
||||
## Unity 与纯 .NET
|
||||
|
||||
### EventBus(静态门面)
|
||||
|
||||
#### 注册 / 注销
|
||||
Unity 包依赖 UniTask:
|
||||
|
||||
```csharp
|
||||
// 同步 handler
|
||||
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
|
||||
EventBus.RegisterEvent<TEvent>(Action<TEvent> handler, int priority);
|
||||
EventBus.SubscribeEvent<TEvent>(Action<TEvent> handler, EventPriority priority, bool receiveCanceled);
|
||||
EventBus.SubscribeEvent<TEvent>(Action<TEvent> handler, int priority);
|
||||
|
||||
// 异步 handler(UniTask)
|
||||
EventBus.RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
|
||||
EventBus.SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority, bool receiveCanceled);
|
||||
|
||||
// 注销
|
||||
EventBus.UnregisterEvent<TEvent>(Action<TEvent> handler);
|
||||
EventBus.UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler);
|
||||
EventBus.UnregisterAllEventsForObject(object target); // 注销某实例的全部 handler
|
||||
EventBus.ClearAllSubscribersForEvent<TEvent>(); // 清空某事件的全部订阅者
|
||||
EventBus.UnregisterAllEvents(); // 全部清空(谨慎使用)
|
||||
UniTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||||
```
|
||||
|
||||
#### 触发
|
||||
纯 .NET 实现在 `DotNet/ShrinkEventBus.Core`:
|
||||
|
||||
```csharp
|
||||
// 同步触发:只同步等待 sync handler;async handler 会基于事件快照 fire-and-forget
|
||||
bool handled = EventBus.TriggerEvent<TEvent>(TEvent eventArgs);
|
||||
|
||||
// 异步触发:顺序 await 每个 handler
|
||||
bool handled = await EventBus.TriggerEventAsync<TEvent>(TEvent eventArgs);
|
||||
ValueTask<ShrinkPostResult> PostAsync<TEvent>(...)
|
||||
```
|
||||
|
||||
> ⚠️ `TriggerEvent` 中遇到 async handler 时,不会等待其完成,而是对当前事件做一份快照后异步执行。如果你需要让 async handler 参与最终状态(如 `IsCanceled` / `Result` / 后续字段改写),请使用 `TriggerEventAsync`。
|
||||
两边共享事件与特性契约,awaitable 和调度实现按宿主选择,不把 UniTask/ValueTask 写进事件数据。
|
||||
|
||||
#### 查询
|
||||
## ECS/Burst
|
||||
|
||||
`com.cneicy.shrink-eventbus-entities` 提供:
|
||||
|
||||
```csharp
|
||||
EventBus.IsInstanceRegistered(object target);
|
||||
EventBus.GetRegisteredInstanceCount();
|
||||
EventBus.GetRegisteredEventTypeCount();
|
||||
EventBus.GetEventSubscribers<TEvent>(); // 返回 EventHandlerInfo[]
|
||||
EventBus.GetListenerList<TEvent>(); // 无订阅者时返回 null
|
||||
EventBus.GetActiveSubscriptionsSnapshot();// 返回 IDisposable 订阅快照
|
||||
ShrinkEcsEventWriter<TEvent>
|
||||
ShrinkEcsEventQueue<TEvent>.Playback(IShrinkEventBus bus)
|
||||
```
|
||||
|
||||
### EventPool\<T\>
|
||||
Burst Job 只写 unmanaged 事实事件到 NativeQueue;Playback 在托管/ECS 系统阶段进入相同 Bus。取消、结果或等待异步完成的事件不应从 Burst Job 直接发布。
|
||||
|
||||
## 队列与关闭
|
||||
|
||||
`ShrinkBusOptions` 配置队列容量、溢出策略、最大并发、关闭排空和超时。关键业务默认使用 `Reject`;`DropNewest/DropOldest` 只适用于明确允许丢失的数据;`Wait` 只由 `PostAsync` 使用。
|
||||
|
||||
## UI Toolkit 调试器
|
||||
|
||||
菜单:`ShrinkSDK/事件总线/事件查看器`。
|
||||
|
||||
窗口使用密集的 Bus 导航、事件流和事件详情三段布局,提供 Bus/Scheduler/DispatchMode/队列/溢出策略/handler 数量、实时速率、平均分发耗时、同步或异步执行方式、实际线程、`ShrinkPostResult`、问题筛选、跟随、搜索、CSV 导出与最多 5000 条环形保留。详细采样只在窗口开启且 `Capture` 为启用状态时生效,暂停或关闭窗口后不计时、不保留事件对象。
|
||||
|
||||
它只观察静态 `EventBus` 宿主管理的 Bus,不捕获独立 `ShrinkEventBusHost`;性能基准仍使用独立 Host,避免 UI 采样进入被测热路径。
|
||||
|
||||
## Benchmark
|
||||
|
||||
实现位于 `Benchmark/ShrinkEventBusBenchmark.cs`,入口:
|
||||
|
||||
```csharp
|
||||
// 从池中取出(自动重置状态)
|
||||
var evt = EventPool<MyEvent>.Get();
|
||||
|
||||
// 手动归还
|
||||
EventPool<MyEvent>.Release(evt);
|
||||
|
||||
// 推荐:配合 using 自动归还
|
||||
using var evt = EventPool<MyEvent>.Get();
|
||||
EventBus.TriggerEvent(evt);
|
||||
// 作用域结束时调用 Dispose() → 自动归还
|
||||
ShrinkEventBusBenchmark.Run(
|
||||
iterations: 1_000_000,
|
||||
massIterations: 10_000,
|
||||
registrationIterations: 5_000);
|
||||
```
|
||||
|
||||
> ⚠️ 归还后不要再访问 `evt` 的属性,对象已被重置并放回池中。
|
||||
2026-08-24 在 Unity `2022.3.62f3`、Windows Editor Play Mode、独立 Inline Host 下三轮中位数(全局调试采样不观察该 Host):
|
||||
|
||||
### EventBase 关键成员
|
||||
| 场景 | 2.0 ops/s | 1.3.0 基线 |
|
||||
|---|---:|---:|
|
||||
| 0 handler | 129,282,649 | 8,971,378 |
|
||||
| 1 handler | 67,249,586 | 6,132,242 |
|
||||
| 8 handlers, struct | 35,024,798 | 无同口径数据 |
|
||||
| 8 handlers, class | 25,321,199 | MessagePipe 图示 25,639,260 |
|
||||
| 30 handlers | 10,823,612 | 258,805 |
|
||||
| canceled skips 10 | 9,029,517 | 1,894,776 |
|
||||
| 1 async handler | 5,909,446 | 1,168,634 |
|
||||
| generated Attach + Dispose | 674,291 | instance scan 76,353 |
|
||||
|
||||
```csharp
|
||||
evt.EventId // Guid,每次派发唯一(懒生成,首次访问时分配)
|
||||
evt.EventTime // 事件创建时间(UTC)
|
||||
evt.IsCancelable // 是否支持取消(由 [Cancelable] 决定)
|
||||
evt.HasResult // 是否支持结果(由 [HasResult] 决定)
|
||||
evt.IsCanceled // 是否已被取消
|
||||
evt.Result // 当前结果(EventResult 枚举)
|
||||
evt.Phase // 当前执行到的优先级阶段
|
||||
evt.CurrentHandler // 当前正在执行的 handler 信息
|
||||
evt.GetSubscribers() // 获取本次派发的 handler 快照拷贝(调试用)
|
||||
```
|
||||
同步 struct `Post` 热路径实测为 `0 B / 10,000,000 ops`;全局门面在没有 Network bridge 对象观察者时也不会为 struct 事件装箱。MessagePipe 图来自不同机器和 .NET benchmark,class 结果只能说明已达到同一吞吐量级,不能作为跨设备胜负结论。当前 Windows x64 Development IL2CPP Player 已构建成功;仍应在目标平台 Player/设备上复测吞吐。
|
||||
|
||||
---
|
||||
## 设计边界
|
||||
|
||||
## 🏗️ 架构说明
|
||||
- handler 不单独选择线程;需要不同线程亲和性时发布到另一个 Bus。
|
||||
- Parallel 分发不承诺优先级顺序。
|
||||
- 同步 `Post` 遇到异步 handler 时是 fire-and-forget;需要等待使用 `PostAsync`。
|
||||
- 外部 Mod DLL 必须携带生成合同;没有合同的生产 DLL 不自动反射注册。
|
||||
- Editor 诊断是可选观察层,不参与正式无诊断热路径。
|
||||
|
||||
```
|
||||
ShrinkEventBus
|
||||
├── Runtime/
|
||||
│ ├── EventBus 静态门面,内部是一个默认 IShrinkEventBus 实例
|
||||
│ ├── ShrinkEventBusInstance 总线实现:注册、派发、异常策略、phase 分发
|
||||
│ ├── ShrinkEventBusBuilder 实例总线的构建与配置入口
|
||||
│ ├── ListenerList 按 phase 分桶的有序 handler 列表,带快照缓存与父链合并
|
||||
│ ├── EventHandlerInfo 单个 handler 的元信息(优先级、预编译 invoker、调试信息)
|
||||
│ ├── EventBase 所有事件的基类,携带生命周期状态与派发快照
|
||||
│ ├── EventPool<T> 对象池,高频事件减少 GC
|
||||
│ ├── EventCloneUtility 同步路径上 async handler 的事件快照克隆
|
||||
│ ├── EventBusRegHelper 反射扫描 & handler 注册逻辑
|
||||
│ └── EventAutoRegHelper 运行时初始化,确保 IsInitialized 状态正确
|
||||
│
|
||||
├── Editor/
|
||||
│ └── EventBusViewerWindow 事件查看器,实时显示订阅者与触发日志
|
||||
│
|
||||
└── (织入)ShrinkShared.CodeGen / CodeGen 共享 ILPostProcessor 优先,本地 ILPostProcessor 兜底
|
||||
向 [EventBusSubscriber] 类注入 Awake(AutoRegister)
|
||||
与 OnDestroy(UnregisterInstance)
|
||||
```
|
||||
|
||||
**热路径(`TriggerEvent`)工作流:**
|
||||
|
||||
```
|
||||
TriggerEvent(evt)
|
||||
└─ 取该事件类型的 ListenerList // 总线级字典 + 共享锁,每类型常数开销
|
||||
└─ GetHandlers() // 返回缓存快照数组(脏时才重建),无拷贝
|
||||
├─ 快照数组引用挂到事件对象上(一次赋值,供 GetSubscribers 调试)
|
||||
└─ 遍历 handlers[]
|
||||
├─ 跳过已取消 & 不接收取消的 handler
|
||||
├─ Action<T> → 经预编译 invoker 直接调用
|
||||
└─ Func<T, UniTask> → 克隆事件快照后 .Forget()(同步路径)
|
||||
```
|
||||
|
||||
**自动注册完整流程:**
|
||||
|
||||
```
|
||||
【编译期】若当前程序集命中 ShrinkShared.CodeGen 覆盖范围,则由共享 ILPostProcessor 扫描;
|
||||
否则由 CodeGen/EventBusILPostProcessor.cs 本地扫描
|
||||
└─ 找到标记了 [EventBusSubscriber] 且存在实例 [EventSubscribe] 方法的 MonoBehaviour 子类
|
||||
├─ 在 Awake 头部织入 EventBus.AutoRegister(this)
|
||||
└─ 在 OnDestroy 头部织入 EventBus.UnregisterInstance(this)
|
||||
(类无对应方法时自动生成,有虚基类方法时自动调用 base)
|
||||
※ 只引用 ShrinkEventBus.Runtime 的纯业务程序集目前不在织入范围内,需手动 AutoRegister
|
||||
|
||||
【运行时 - 默认静态总线启动】
|
||||
└─ 读取编译期静态订阅清单,注册 static [EventSubscribe] 方法
|
||||
|
||||
【运行时 - 动态创建】Instantiate(prefab)
|
||||
└─ Unity 调用新对象的 Awake(已含织入代码)→ 自动注册
|
||||
|
||||
【运行时 - 销毁】GameObject.Destroy
|
||||
└─ OnDestroy(已含织入代码)→ UnregisterInstance → 自动反注册
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 最佳实践
|
||||
|
||||
**事件设计:尽量让属性只读**
|
||||
|
||||
```csharp
|
||||
// ✅ 推荐:构造时传入,防止 handler 间意外修改输入数据
|
||||
public class OrderPlacedEvent : EventBase
|
||||
{
|
||||
public string OrderId { get; }
|
||||
public decimal Amount { get; }
|
||||
public OrderPlacedEvent(string orderId, decimal amount)
|
||||
{
|
||||
OrderId = orderId;
|
||||
Amount = amount;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ 避免:公开可写属性,handler 间耦合风险高
|
||||
public class BadEvent : EventBase
|
||||
{
|
||||
public object Payload { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**高频事件一定要用对象池**
|
||||
|
||||
```csharp
|
||||
// ✅ 每帧触发的伤害/移动事件
|
||||
using var dmgEvt = EventPool<DamageEvent>.Get();
|
||||
dmgEvt.Value = damage;
|
||||
EventBus.TriggerEvent(dmgEvt);
|
||||
|
||||
// ❌ 每帧 new,会产生大量 GC
|
||||
EventBus.TriggerEvent(new DamageEvent { Value = damage });
|
||||
```
|
||||
|
||||
**非 MonoBehaviour 类一定要手动清理**
|
||||
|
||||
```csharp
|
||||
public void Dispose()
|
||||
{
|
||||
EventBus.UnregisterAllEventsForObject(this);
|
||||
}
|
||||
```
|
||||
|
||||
**异步 handler 中谨慎触发新事件**
|
||||
|
||||
在 `TriggerEventAsync` 的 handler 内部再次 `await TriggerEventAsync`,链条过深时调用栈难以追踪,建议把二次触发拆到外部或改用消息队列。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项
|
||||
|
||||
- **`TriggerEvent` 不等待异步 handler**:同步路径中的 UniTask handler 会基于事件快照异步执行,执行结果和异常不会传回调用方,对原事件对象的改动也不会回写。需要等待并拿到最终状态时请使用 `TriggerEventAsync`。
|
||||
- **同步路径中的 async 快照是浅拷贝**:事件对象本身会复制一份,但如果载荷里挂着可变引用类型(如 `List<>`、`Dictionary<>`、自定义引用对象),内部成员仍然是共享引用。高风险数据建议改成不可变载荷,或统一走 `TriggerEventAsync`。
|
||||
- **EventPool 归还后不要再使用**:`Release` 后对象会立即 `ResetInternal()`,继续访问属性将得到默认值。
|
||||
- **不要在 handler 内直接注册/注销 handler**:可能影响当前正在遍历的 handler 快照,会产生语义上的不确定性。
|
||||
- **静态 handler 永远不会自动注销**:静态方法注册后持续存活直到显式调用 `UnregisterEvent`,不要在静态 handler 里持有场景对象引用。
|
||||
- **`[EventBusSubscriber]` 仅对 MonoBehaviour 生效自动注册**:非 MonoBehaviour 类标记该 Attribute 无任何效果,请使用手动注册。
|
||||
- **标了 `[EventBusSubscriber]` 但没有实例 `[EventSubscribe]` 方法的类**:编译期不会织入;若通过 `AutoRegister` 手动接入,会输出警告并跳过(不抛异常)。显式 `Register()` 对此仍严格抛错。
|
||||
- **int 数字优先级重载必须显式传值**:1.3.0 起 int 重载不再有默认值;数字 `0` 映射 `NORMAL`。
|
||||
- **ILPostProcessor 织入发生在编译期**:修改代码后需要重新编译才能使注入生效,热重载场景下请注意这一点。
|
||||
- **继承泛型基类(如 `Singleton<T>`)时无需额外处理**:ILP 会正确识别泛型基类中的虚方法并生成 `protected override`,自动调用 `base.Awake()` 和 `base.OnDestroy()`。
|
||||
|
||||
---
|
||||
|
||||
## 🐛 常见问题排查
|
||||
|
||||
**事件没有被任何 handler 接收**
|
||||
|
||||
1. 检查订阅类是否有 `[EventBusSubscriber]`
|
||||
2. 检查方法是否有 `[EventSubscribe]`,且签名为 `void/UniTask Method(TEvent evt)`
|
||||
3. 确认代码在标记 `[EventBusSubscriber]` 后重新编译过(ILPostProcessor 需要编译期运行)
|
||||
4. 确认没有在 `Awake` 之前就触发事件
|
||||
|
||||
```csharp
|
||||
// 调试:主动检查注册状态
|
||||
Debug.Log(EventBus.IsInstanceRegistered(this));
|
||||
Debug.Log($"订阅者数量: {EventBus.GetEventSubscribers<MyEvent>().Length}");
|
||||
```
|
||||
|
||||
**怀疑内存泄漏**
|
||||
|
||||
```csharp
|
||||
// 检查是否有 handler 持有意外引用
|
||||
var handlers = EventBus.GetEventSubscribers<MyEvent>();
|
||||
foreach (var h in handlers)
|
||||
Debug.Log($"{h.DisplayDeclaringType.Name}.{h.DisplayMethodName} | target: {h.Target}");
|
||||
```
|
||||
|
||||
**Editor 下想追踪事件流**
|
||||
|
||||
打开事件查看器:菜单栏 → `ShrinkSDK` → `事件总线` → `事件查看器`
|
||||
|
||||
也可以通过代码追踪:
|
||||
|
||||
```csharp
|
||||
EventBus.EnableDebugRecord = true;
|
||||
EventBus.TriggerEvent(evt);
|
||||
foreach (var h in evt.GetSubscribers())
|
||||
Debug.Log($"[{h.Priority}] {h.DisplayDeclaringType.Name}.{h.DisplayMethodName}");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
[MIT](LICENSE)
|
||||
架构审计与来源说明见 `Docs/JustAnyProjectArchitectureAudit.md` 和根目录 `DESIGN.md`。
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ShrinkNetwork.Integration.EventBus")]
|
||||
[assembly: InternalsVisibleTo("ShrinkEventBus.Editor")]
|
||||
[assembly: InternalsVisibleTo("ShrinkEventBus.Tests")]
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class CancelableAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class HasResultAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class EventBusSubscriberAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
|
||||
public class EventSubscribeAttribute : Attribute
|
||||
{
|
||||
public EventPriority Priority { get; set; }
|
||||
public bool ReceiveCanceled { get; set; }
|
||||
public int NumericPriority { get; set; }
|
||||
|
||||
public EventSubscribeAttribute()
|
||||
{
|
||||
Priority = EventPriority.NORMAL;
|
||||
ReceiveCanceled = false;
|
||||
NumericPriority = 0;
|
||||
}
|
||||
|
||||
public EventSubscribeAttribute(EventPriority priority, bool receiveCanceled = false)
|
||||
{
|
||||
Priority = priority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
NumericPriority = 0;
|
||||
}
|
||||
|
||||
public EventSubscribeAttribute(int priority)
|
||||
{
|
||||
NumericPriority = priority;
|
||||
Priority = PriorityHelper.ConvertToEventPriority(priority);
|
||||
ReceiveCanceled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bb9d82b04a04f3085b9e68ab4f1ba60
|
||||
timeCreated: 1760098802
|
||||
@@ -1,36 +0,0 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public static class EventAutoRegHelper
|
||||
{
|
||||
private static readonly object InitLock = new();
|
||||
public static bool IsInitialized { get; private set; }
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
||||
private static void InitializeAfterSceneLoad()
|
||||
{
|
||||
EnsureInitialized();
|
||||
}
|
||||
|
||||
public static void EnsureInitialized()
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
lock (InitLock)
|
||||
{
|
||||
if (IsInitialized) return;
|
||||
IsInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
lock (InitLock)
|
||||
{
|
||||
IsInitialized = false;
|
||||
}
|
||||
|
||||
EventBus.UnregisterAllEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f4d79f65274145cab7614950a64a92f5
|
||||
timeCreated: 1760099405
|
||||
@@ -1,146 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public abstract class EventBase : IDisposable
|
||||
{
|
||||
private sealed class EventMetadata
|
||||
{
|
||||
public bool IsCancelable;
|
||||
public bool HasResult;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<Type, EventMetadata> MetadataCache = new();
|
||||
|
||||
private EventHandlerInfo[]? _dispatchSnapshot;
|
||||
private Guid? _eventId;
|
||||
private bool _isCanceled;
|
||||
private EventResult _result = EventResult.DEFAULT;
|
||||
|
||||
[IgnoreDataMember]
|
||||
public EventHandlerInfo? CurrentHandler { get; internal set; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public DateTime EventTime { get; private set; } = DateTime.UtcNow;
|
||||
|
||||
[IgnoreDataMember]
|
||||
public Guid EventId => _eventId ??= Guid.NewGuid();
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool IsCancelable { get; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool HasResult { get; }
|
||||
|
||||
[IgnoreDataMember]
|
||||
public EventPriority? Phase { get; private set; }
|
||||
|
||||
internal bool IsInPool { get; set; }
|
||||
internal Action<EventBase>? ReleaseAction { get; set; }
|
||||
|
||||
protected EventBase()
|
||||
{
|
||||
var metadata = GetOrCreateMetadata(GetType());
|
||||
IsCancelable = metadata.IsCancelable;
|
||||
HasResult = metadata.HasResult;
|
||||
Setup();
|
||||
}
|
||||
|
||||
protected virtual void Setup() { }
|
||||
|
||||
protected virtual void OnReset() { }
|
||||
|
||||
internal void ResetInternal()
|
||||
{
|
||||
_isCanceled = false;
|
||||
_result = EventResult.DEFAULT;
|
||||
CurrentHandler = null;
|
||||
Phase = null;
|
||||
EventTime = DateTime.UtcNow;
|
||||
_eventId = null;
|
||||
_dispatchSnapshot = null;
|
||||
OnReset();
|
||||
}
|
||||
|
||||
internal void PrepareForDispatch()
|
||||
{
|
||||
CurrentHandler = null;
|
||||
Phase = null;
|
||||
EventTime = DateTime.UtcNow;
|
||||
_eventId = null;
|
||||
_dispatchSnapshot = null;
|
||||
}
|
||||
|
||||
internal void SetListenerSnapshot(EventHandlerInfo[]? handlers)
|
||||
{
|
||||
_dispatchSnapshot = handlers is { Length: > 0 } ? handlers : null;
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public bool IsCanceled
|
||||
{
|
||||
get => _isCanceled;
|
||||
set
|
||||
{
|
||||
if (!IsCancelable)
|
||||
throw new UnsupportedOperationException(
|
||||
$"Event {GetType().Name} is not cancelable. Mark it with [Cancelable] to allow cancellation.");
|
||||
_isCanceled = value;
|
||||
}
|
||||
}
|
||||
|
||||
[IgnoreDataMember]
|
||||
public EventResult Result
|
||||
{
|
||||
get => _result;
|
||||
set
|
||||
{
|
||||
if (!HasResult)
|
||||
throw new InvalidOperationException(
|
||||
$"Event {GetType().Name} does not support results. Mark it with [HasResult] to allow setting a result.");
|
||||
_result = value;
|
||||
}
|
||||
}
|
||||
|
||||
internal void SetPhase(EventPriority value)
|
||||
{
|
||||
if (Phase == value) return;
|
||||
if (Phase != null && Phase.Value.CompareTo(value) > 0)
|
||||
throw new ArgumentException(
|
||||
$"Event phase cannot move backwards from {Phase.Value} to {value}.", nameof(value));
|
||||
Phase = value;
|
||||
}
|
||||
|
||||
public void SetCanceled(bool canceled) => IsCanceled = canceled;
|
||||
public void SetResult(EventResult result) => Result = result;
|
||||
|
||||
public EventHandlerInfo[] GetSubscribers()
|
||||
{
|
||||
var snapshot = _dispatchSnapshot;
|
||||
if (snapshot == null || snapshot.Length == 0)
|
||||
return Array.Empty<EventHandlerInfo>();
|
||||
|
||||
var copy = new EventHandlerInfo[snapshot.Length];
|
||||
Array.Copy(snapshot, copy, snapshot.Length);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ReleaseAction?.Invoke(this);
|
||||
}
|
||||
|
||||
private static EventMetadata GetOrCreateMetadata(Type eventType)
|
||||
{
|
||||
return MetadataCache.GetOrAdd(eventType, static type => new EventMetadata
|
||||
{
|
||||
IsCancelable = type.GetCustomAttribute<CancelableAttribute>() != null,
|
||||
HasResult = type.GetCustomAttribute<HasResultAttribute>() != null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b978c5174cf44d893a41950e7a504e3
|
||||
timeCreated: 1760098714
|
||||
@@ -1,129 +1,196 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal readonly struct ShrinkEventTrace
|
||||
{
|
||||
public ShrinkEventTrace(DateTime timestampUtc, Type eventType, ShrinkBusKey busKey,
|
||||
ShrinkBusOptions options, int threadId, long elapsedTimestampTicks,
|
||||
ShrinkPostResult result, bool isAsync)
|
||||
{
|
||||
TimestampUtc = timestampUtc;
|
||||
EventType = eventType;
|
||||
BusKey = busKey;
|
||||
Scheduler = options.Scheduler;
|
||||
DispatchMode = options.DispatchMode;
|
||||
ThreadId = threadId;
|
||||
ElapsedTimestampTicks = elapsedTimestampTicks;
|
||||
Result = result;
|
||||
IsAsync = isAsync;
|
||||
}
|
||||
|
||||
public DateTime TimestampUtc { get; }
|
||||
public Type EventType { get; }
|
||||
public ShrinkBusKey BusKey { get; }
|
||||
public ShrinkBusSchedulerKind Scheduler { get; }
|
||||
public ShrinkDispatchMode DispatchMode { get; }
|
||||
public int ThreadId { get; }
|
||||
public long ElapsedTimestampTicks { get; }
|
||||
public ShrinkPostResult Result { get; }
|
||||
public bool IsAsync { get; }
|
||||
}
|
||||
|
||||
public static class EventBus
|
||||
{
|
||||
private sealed class GlobalBusResolver : IShrinkBusResolver
|
||||
{
|
||||
public IShrinkEventBus GetBus(ShrinkBusKey key)
|
||||
{
|
||||
if (TryGetBus(key, out var bus))
|
||||
return bus;
|
||||
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
|
||||
}
|
||||
|
||||
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
|
||||
EventBus.TryGetBus(key, out bus);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> Buses = new();
|
||||
internal static readonly IShrinkBusResolver Resolver = new GlobalBusResolver();
|
||||
private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus();
|
||||
private static Action<IShrinkEvent, Type, ShrinkBusKey>? _posted;
|
||||
private static Action<ShrinkEventTrace>? _detailedPosted;
|
||||
|
||||
internal static event Action<IShrinkEvent, Type, ShrinkBusKey> Posted
|
||||
{
|
||||
add
|
||||
{
|
||||
_posted += value;
|
||||
}
|
||||
remove
|
||||
{
|
||||
_posted -= value;
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool HasPostedObservers
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Volatile.Read(ref _posted) != null;
|
||||
}
|
||||
|
||||
internal static event Action<ShrinkEventTrace> DetailedPosted
|
||||
{
|
||||
add => _detailedPosted += value;
|
||||
remove => _detailedPosted -= value;
|
||||
}
|
||||
|
||||
internal static bool HasDetailedPostedObservers
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => Volatile.Read(ref _detailedPosted) != null;
|
||||
}
|
||||
|
||||
public static IShrinkEventBus Default => DefaultBus;
|
||||
|
||||
public static event Action<EventBase, Type> OnEventTriggered
|
||||
public static IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||
{
|
||||
add => DefaultBus.OnEventTriggered += value;
|
||||
remove => DefaultBus.OnEventTriggered -= value;
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
var bus = BuildBus(key, options);
|
||||
if (!Buses.TryAdd(key, bus))
|
||||
{
|
||||
bus.Dispose();
|
||||
throw new InvalidOperationException($"Bus '{key}' is already registered.");
|
||||
}
|
||||
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
|
||||
return bus;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public static bool EnableDebugRecord
|
||||
public static IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||
{
|
||||
get => DefaultBus.EnableDebugRecord;
|
||||
set => DefaultBus.EnableDebugRecord = value;
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
var bus = Buses.GetOrAdd(key, busKey => BuildBus(busKey, options));
|
||||
ShrinkStaticBindingRegistry.AttachForBus(key, Resolver);
|
||||
return bus;
|
||||
}
|
||||
|
||||
public static event Action<EventBase, string, string, EventHandlerInfo[]> OnEventTriggeredForEditor
|
||||
{
|
||||
add => DefaultBus.OnEventTriggeredForEditor += value;
|
||||
remove => DefaultBus.OnEventTriggeredForEditor -= value;
|
||||
}
|
||||
#endif
|
||||
public static bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) =>
|
||||
Buses.TryGetValue(key, out bus!);
|
||||
|
||||
public static ShrinkEventBusBuilder Builder() => new();
|
||||
|
||||
public static IShrinkEventBus CreateBus(Action<ShrinkEventBusBuilder>? configure = null)
|
||||
public static bool RemoveBus(ShrinkBusKey key)
|
||||
{
|
||||
var builder = new ShrinkEventBusBuilder();
|
||||
configure?.Invoke(builder);
|
||||
return builder.Build();
|
||||
if (key == ShrinkBusKey.Game || !Buses.TryRemove(key, out var bus))
|
||||
return false;
|
||||
ShrinkStaticBindingRegistry.DetachBus(key);
|
||||
bus.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void Start() => DefaultBus.Start();
|
||||
public static void AutoRegister(object target) => DefaultBus.AutoRegister(target);
|
||||
public static void Register(object target) => DefaultBus.Register(target);
|
||||
public static void Unregister(object target) => DefaultBus.Unregister(target);
|
||||
public static ShrinkPostResult Post<TEvent>(in TEvent eventData)
|
||||
where TEvent : IShrinkEvent => DefaultBus.Post(in eventData);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
|
||||
public static UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TEvent : IShrinkEvent => DefaultBus.PostAsync(eventData, cancellationToken);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Action<TEvent> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled);
|
||||
public static IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
|
||||
{
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
if (target is not IShrinkGeneratedSubscriber generated)
|
||||
throw new InvalidOperationException(
|
||||
$"Type {target.GetType().FullName} has no generated [ShrinkSubscribe] binding.");
|
||||
return generated.AttachGenerated(Resolver, defaultBus ?? ShrinkBusKey.Game);
|
||||
}
|
||||
|
||||
public static void RegisterEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority);
|
||||
|
||||
public static void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||
where TEvent : EventBase => DefaultBus.RegisterEvent(handler, priority, receiveCanceled);
|
||||
|
||||
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled);
|
||||
|
||||
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority);
|
||||
|
||||
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false)
|
||||
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority, receiveCanceled);
|
||||
|
||||
public static IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||
where TEvent : EventBase => DefaultBus.SubscribeEvent(handler, priority);
|
||||
|
||||
public static void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase =>
|
||||
DefaultBus.UnregisterEvent(handler);
|
||||
|
||||
public static void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase =>
|
||||
DefaultBus.UnregisterEvent(handler);
|
||||
|
||||
public static void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase =>
|
||||
DefaultBus.ClearAllSubscribersForEvent<TEvent>();
|
||||
|
||||
public static void UnregisterAllEventsForObject(object targetObject) =>
|
||||
DefaultBus.UnregisterAllEventsForObject(targetObject);
|
||||
|
||||
public static void UnregisterInstance(object targetObject) => DefaultBus.Unregister(targetObject);
|
||||
public static void UnregisterAllEvents() => DefaultBus.UnregisterAllEvents();
|
||||
|
||||
public static UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase =>
|
||||
DefaultBus.TriggerEventAsync(eventArgs);
|
||||
|
||||
public static UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs)
|
||||
where TEvent : EventBase => DefaultBus.TriggerEventAsync(phase, eventArgs);
|
||||
|
||||
public static bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase =>
|
||||
DefaultBus.TriggerEvent(eventArgs);
|
||||
|
||||
public static bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase =>
|
||||
DefaultBus.TriggerEvent(phase, eventArgs);
|
||||
|
||||
public static EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase =>
|
||||
DefaultBus.GetEventSubscribers<TEvent>();
|
||||
|
||||
public static ListenerList GetListenerList<TEvent>() where TEvent : EventBase =>
|
||||
DefaultBus.GetListenerList<TEvent>();
|
||||
|
||||
public static IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot() =>
|
||||
DefaultBus.GetAllSubscribersSnapshot();
|
||||
|
||||
public static IReadOnlyList<ShrinkEventSubscriptionSnapshot> GetActiveSubscriptionsSnapshot() =>
|
||||
DefaultBus.GetActiveSubscriptionsSnapshot();
|
||||
|
||||
public static bool IsInstanceRegistered(object target) => DefaultBus.IsInstanceRegistered(target);
|
||||
public static int GetRegisteredInstanceCount() => DefaultBus.GetRegisteredInstanceCount();
|
||||
public static int GetRegisteredEventTypeCount() => DefaultBus.GetRegisteredEventTypeCount();
|
||||
internal static IReadOnlyList<(ShrinkBusKey Key, ShrinkBusOptions Options, int Subscribers)> Snapshot()
|
||||
{
|
||||
var snapshot = new List<(ShrinkBusKey, ShrinkBusOptions, int)>(Buses.Count);
|
||||
foreach (var pair in Buses)
|
||||
{
|
||||
var subscribers = pair.Value is ShrinkEventBusInstance instance
|
||||
? instance.SubscriberCount
|
||||
: 0;
|
||||
snapshot.Add((pair.Key, pair.Value.Options, subscribers));
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static IShrinkEventBus CreateDefaultBus()
|
||||
{
|
||||
var bus = (ShrinkEventBusInstance)new ShrinkEventBusBuilder()
|
||||
.SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode.LogAndContinue)
|
||||
.AllowPerPhaseDispatch()
|
||||
.Build();
|
||||
EventBusRegHelper.RegStaticEventHandler(bus);
|
||||
var bus = BuildBus(ShrinkBusKey.Game, ShrinkBusOptions.MainThread());
|
||||
Buses[ShrinkBusKey.Game] = bus;
|
||||
ShrinkStaticBindingRegistry.AttachForBus(ShrinkBusKey.Game, Resolver);
|
||||
return bus;
|
||||
}
|
||||
|
||||
private static IShrinkEventBus BuildBus(ShrinkBusKey key, ShrinkBusOptions options) =>
|
||||
new ShrinkEventBusBuilder()
|
||||
.WithKey(key)
|
||||
.WithOptions(options)
|
||||
.WithPostObserver(NotifyPostedObject)
|
||||
.Build();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void NotifyPostedObject(IShrinkEvent eventData, Type eventType, ShrinkBusKey key)
|
||||
{
|
||||
_posted?.Invoke(eventData, eventType, key);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void NotifyDetailedPosted(ShrinkEventTrace trace)
|
||||
{
|
||||
var observers = Volatile.Read(ref _detailedPosted);
|
||||
if (observers == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
observers(trace);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
UnityEngine.Debug.LogException(exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,519 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using Debug = UnityEngine.Debug;
|
||||
|
||||
namespace ShrinkEventBus.Runtime
|
||||
{
|
||||
/// <summary>
|
||||
/// ShrinkEventBus 性能测试。
|
||||
/// 挂在任意 GameObject 上,运行后在 Console 查看结果。
|
||||
/// 在进行测试时不要打开事件查看器的大量实时追踪。
|
||||
/// </summary>
|
||||
public sealed class EventBusBenchmark : MonoBehaviour
|
||||
{
|
||||
[Header("同步触发测试")] public int iterations = 1_000_000;
|
||||
|
||||
[Header("大批量订阅者测试")] public int subscriberCount = 30;
|
||||
public int massIterations = 10_000;
|
||||
|
||||
[Header("异步触发测试")] public int asyncIterations = 1_000;
|
||||
|
||||
[Header("注册 / 注销测试")] public int registerIterations = 5_000;
|
||||
public int scannedRegisterIterations = 1_000;
|
||||
|
||||
[Header("取消事件测试")] public int cancelIterations = 100_000;
|
||||
|
||||
public class BenchmarkEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
public sealed class DerivedBenchmarkEvent : BenchmarkEvent
|
||||
{
|
||||
public bool IsDerived { get; set; }
|
||||
}
|
||||
|
||||
[Cancelable]
|
||||
public class CancelableBenchmarkEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BenchmarkInstanceSubscriber
|
||||
{
|
||||
public int Recorded;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnBenchmark(BenchmarkEvent evt)
|
||||
{
|
||||
Recorded = evt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class BenchmarkStaticSubscriber
|
||||
{
|
||||
public static int Recorded;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private static void OnBenchmark(BenchmarkEvent evt)
|
||||
{
|
||||
Recorded = evt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static class BenchmarkMethodSubscriber
|
||||
{
|
||||
public static int Recorded;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private static void OnBenchmark(BenchmarkEvent evt)
|
||||
{
|
||||
Recorded = evt.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PhaseSubscriber
|
||||
{
|
||||
public int Sum;
|
||||
|
||||
[EventSubscribe(EventPriority.HIGH)]
|
||||
private void OnHigh(BenchmarkEvent evt)
|
||||
{
|
||||
Sum += evt.Value + 1;
|
||||
}
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnNormal(BenchmarkEvent evt)
|
||||
{
|
||||
Sum += evt.Value + 10;
|
||||
}
|
||||
|
||||
[EventSubscribe(EventPriority.LOW)]
|
||||
private void OnLow(BenchmarkEvent evt)
|
||||
{
|
||||
Sum += evt.Value + 100;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly StringBuilder _report = new();
|
||||
|
||||
private void Start()
|
||||
{
|
||||
RunAllBenchmarks().Forget();
|
||||
}
|
||||
|
||||
private async UniTaskVoid RunAllBenchmarks()
|
||||
{
|
||||
_report.Clear();
|
||||
_report.AppendLine("╔══════════════════════════════════════════════════╗");
|
||||
_report.AppendLine("║ ShrinkEventBus Benchmark Report ║");
|
||||
_report.AppendLine(
|
||||
$"║ iter={iterations,8} mass={massIterations,6} reg={registerIterations,5} scan={scannedRegisterIterations,4} ║");
|
||||
_report.AppendLine("╚══════════════════════════════════════════════════╝");
|
||||
_report.AppendLine(" 基准对象:独立实例 bus(AllowPerPhaseDispatch 已开启)");
|
||||
|
||||
Log("开始:无订阅者触发");
|
||||
Bench_TriggerEvent_NoSubscriber();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:单订阅者同步触发");
|
||||
Bench_TriggerEvent_SingleSubscriber();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:父事件监听子事件");
|
||||
Bench_TriggerEvent_InheritedSubscriber();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log($"开始:{subscriberCount} 订阅者同步触发");
|
||||
Bench_TriggerEvent_MassSubscribers();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:按 phase 分发");
|
||||
Bench_TriggerEvent_PerPhase();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:异步触发");
|
||||
await Bench_TriggerEventAsync();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:手工 delegate 注册/注销");
|
||||
Bench_RegisterUnregister_ManualDelegate();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:实例扫描注册/注销");
|
||||
Bench_RegisterUnregister_InstanceScan();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:静态类型扫描注册/注销");
|
||||
Bench_RegisterUnregister_StaticScan();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:MethodInfo 注册/注销");
|
||||
Bench_RegisterUnregister_MethodScan();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:EventPool vs new");
|
||||
Bench_EventPool_vs_New();
|
||||
await UniTask.Yield();
|
||||
|
||||
Log("开始:已取消事件跳过");
|
||||
Bench_CanceledEvent_Skip();
|
||||
|
||||
_report.AppendLine("\n══════════════════════════════════════════════════");
|
||||
_report.AppendLine(" 全部测试完成");
|
||||
Debug.Log(_report.ToString());
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_NoSubscriber()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
|
||||
Warmup(() => bus.TriggerEvent(evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent × 无订阅者(基线)", sw.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:无订阅者触发");
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_SingleSubscriber()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
|
||||
void Handler(BenchmarkEvent evt)
|
||||
{
|
||||
dummy = evt.Value;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
Warmup(() => bus.TriggerEvent(evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent × 单订阅者同步", sw.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:单订阅者同步触发");
|
||||
|
||||
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_InheritedSubscriber()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
|
||||
void Handler(BenchmarkEvent evt)
|
||||
{
|
||||
dummy = evt.Value;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
var evt = new DerivedBenchmarkEvent { Value = 1, IsDerived = true };
|
||||
Warmup(() => bus.TriggerEvent(evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent × 父事件监听子事件", sw.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:父事件监听子事件");
|
||||
|
||||
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_MassSubscribers()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
var handlers = new Action<BenchmarkEvent>[subscriberCount];
|
||||
for (var i = 0; i < subscriberCount; i++)
|
||||
{
|
||||
var captured = i;
|
||||
handlers[i] = evt => { dummy = captured + evt.Value; };
|
||||
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
Warmup(() => bus.TriggerEvent(evt), 100);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < massIterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record($"TriggerEvent × {subscriberCount} 订阅者同步", sw.Elapsed.TotalMilliseconds, massIterations);
|
||||
Log($"完成:{subscriberCount} 订阅者同步触发");
|
||||
|
||||
foreach (var handler in handlers)
|
||||
bus.UnregisterEvent<BenchmarkEvent>(handler);
|
||||
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_TriggerEvent_PerPhase()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var subscriber = new PhaseSubscriber();
|
||||
bus.Register(subscriber);
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
Warmup(() => bus.TriggerEvent(EventPriority.HIGH, evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
bus.TriggerEvent(EventPriority.HIGH, evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent(Phase=HIGH) × 3 phase 监听", sw.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:按 phase 分发");
|
||||
|
||||
bus.Unregister(subscriber);
|
||||
GC.KeepAlive(subscriber.Sum);
|
||||
}
|
||||
|
||||
private async UniTask Bench_TriggerEventAsync()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
|
||||
async UniTask Handler(BenchmarkEvent evt)
|
||||
{
|
||||
dummy = evt.Value;
|
||||
await UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
var evt = new BenchmarkEvent { Value = 1 };
|
||||
await WarmupAsync(() => bus.TriggerEventAsync(evt), 10);
|
||||
|
||||
const int perFrame = 10;
|
||||
var totalMs = 0.0;
|
||||
var ran = 0;
|
||||
|
||||
while (ran < asyncIterations)
|
||||
{
|
||||
await UniTask.Yield();
|
||||
var count = Math.Min(perFrame, asyncIterations - ran);
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < count; i++)
|
||||
await bus.TriggerEventAsync(evt);
|
||||
sw.Stop();
|
||||
totalMs += sw.Elapsed.TotalMilliseconds;
|
||||
ran += count;
|
||||
}
|
||||
|
||||
Record("TriggerEventAsync × 单订阅者异步", totalMs, asyncIterations);
|
||||
Log("完成:异步触发");
|
||||
|
||||
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_ManualDelegate()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
var handlers = new Action<BenchmarkEvent>[registerIterations];
|
||||
for (var i = 0; i < registerIterations; i++)
|
||||
{
|
||||
var captured = i;
|
||||
handlers[i] = evt => { dummy = captured; };
|
||||
}
|
||||
|
||||
var swReg = Stopwatch.StartNew();
|
||||
for (var i = 0; i < registerIterations; i++)
|
||||
bus.RegisterEvent<BenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||
swReg.Stop();
|
||||
Record("RegisterEvent(delegate)", swReg.Elapsed.TotalMilliseconds, registerIterations);
|
||||
|
||||
var swUnreg = Stopwatch.StartNew();
|
||||
for (var i = 0; i < registerIterations; i++)
|
||||
bus.UnregisterEvent<BenchmarkEvent>(handlers[i]);
|
||||
swUnreg.Stop();
|
||||
Record("UnregisterEvent(delegate)", swUnreg.Elapsed.TotalMilliseconds, registerIterations);
|
||||
Log("完成:手工 delegate 注册/注销");
|
||||
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_InstanceScan()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var subscribers = new BenchmarkInstanceSubscriber[scannedRegisterIterations];
|
||||
for (var i = 0; i < subscribers.Length; i++)
|
||||
subscribers[i] = new BenchmarkInstanceSubscriber();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < subscribers.Length; i++)
|
||||
{
|
||||
var subscriber = subscribers[i];
|
||||
bus.Register(subscriber);
|
||||
bus.Unregister(subscriber);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Record("Register/Unregister(instance scan)", sw.Elapsed.TotalMilliseconds, subscribers.Length);
|
||||
Log("完成:实例扫描注册/注销");
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_StaticScan()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < scannedRegisterIterations; i++)
|
||||
{
|
||||
bus.Register(typeof(BenchmarkStaticSubscriber));
|
||||
bus.Unregister(typeof(BenchmarkStaticSubscriber));
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Record("Register/Unregister(static type scan)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations);
|
||||
Log("完成:静态类型扫描注册/注销");
|
||||
}
|
||||
|
||||
private void Bench_RegisterUnregister_MethodScan()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var method = typeof(BenchmarkMethodSubscriber).GetMethod("OnBenchmark",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
if (method == null)
|
||||
throw new MissingMethodException(typeof(BenchmarkMethodSubscriber).FullName, "OnBenchmark");
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < scannedRegisterIterations; i++)
|
||||
{
|
||||
bus.Register(method);
|
||||
bus.Unregister(method);
|
||||
}
|
||||
|
||||
sw.Stop();
|
||||
Record("Register/Unregister(MethodInfo)", sw.Elapsed.TotalMilliseconds, scannedRegisterIterations);
|
||||
Log("完成:MethodInfo 注册/注销");
|
||||
}
|
||||
|
||||
private void Bench_EventPool_vs_New()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
|
||||
void Handler(BenchmarkEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
bus.RegisterEvent<BenchmarkEvent>(Handler, EventPriority.NORMAL);
|
||||
|
||||
for (var i = 0; i < 64; i++)
|
||||
EventPool<BenchmarkEvent>.Release(EventPool<BenchmarkEvent>.Get());
|
||||
|
||||
GC.Collect();
|
||||
var gcBefore = GC.CollectionCount(0);
|
||||
var swNew = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
var evt = new BenchmarkEvent { Value = i };
|
||||
bus.TriggerEvent(evt);
|
||||
}
|
||||
|
||||
swNew.Stop();
|
||||
var gcNew = GC.CollectionCount(0) - gcBefore;
|
||||
Record($"TriggerEvent × new() [GC Gen0={gcNew,3}]", swNew.Elapsed.TotalMilliseconds, iterations);
|
||||
|
||||
GC.Collect();
|
||||
gcBefore = GC.CollectionCount(0);
|
||||
var swPool = Stopwatch.StartNew();
|
||||
for (var i = 0; i < iterations; i++)
|
||||
{
|
||||
using var evt = EventPool<BenchmarkEvent>.Get();
|
||||
evt.Value = i;
|
||||
bus.TriggerEvent(evt);
|
||||
}
|
||||
|
||||
swPool.Stop();
|
||||
var gcPool = GC.CollectionCount(0) - gcBefore;
|
||||
Record($"TriggerEvent × Pool.Get() [GC Gen0={gcPool,3}]", swPool.Elapsed.TotalMilliseconds, iterations);
|
||||
Log("完成:EventPool vs new");
|
||||
|
||||
bus.UnregisterEvent<BenchmarkEvent>(Handler);
|
||||
}
|
||||
|
||||
private void Bench_CanceledEvent_Skip()
|
||||
{
|
||||
var bus = CreateBenchmarkBus();
|
||||
var dummy = 0;
|
||||
var handlers = new Action<CancelableBenchmarkEvent>[10];
|
||||
for (var i = 0; i < handlers.Length; i++)
|
||||
{
|
||||
handlers[i] = evt => { dummy = evt.Value; };
|
||||
bus.RegisterEvent<CancelableBenchmarkEvent>(handlers[i], EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
var evt = new CancelableBenchmarkEvent { Value = 1 };
|
||||
evt.SetCanceled(true);
|
||||
|
||||
Warmup(() => bus.TriggerEvent(evt), 500);
|
||||
|
||||
var sw = Stopwatch.StartNew();
|
||||
for (var i = 0; i < cancelIterations; i++)
|
||||
bus.TriggerEvent(evt);
|
||||
sw.Stop();
|
||||
|
||||
Record("TriggerEvent × 已取消事件跳过(10 订阅者)", sw.Elapsed.TotalMilliseconds, cancelIterations);
|
||||
Log("完成:已取消事件跳过");
|
||||
|
||||
foreach (var handler in handlers)
|
||||
bus.UnregisterEvent<CancelableBenchmarkEvent>(handler);
|
||||
|
||||
GC.KeepAlive(dummy);
|
||||
}
|
||||
|
||||
private static IShrinkEventBus CreateBenchmarkBus()
|
||||
{
|
||||
return EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch());
|
||||
}
|
||||
|
||||
private static void Warmup(Action action, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
action();
|
||||
}
|
||||
|
||||
private static async UniTask WarmupAsync(Func<UniTask<bool>> action, int count)
|
||||
{
|
||||
for (var i = 0; i < count; i++)
|
||||
await action();
|
||||
}
|
||||
|
||||
private static void Log(string msg)
|
||||
{
|
||||
Debug.Log($"[Benchmark] {msg}");
|
||||
}
|
||||
|
||||
private void Record(string label, double totalMs, int count)
|
||||
{
|
||||
var perOp = totalMs / count * 1000.0;
|
||||
var throughput = totalMs <= 0.0001 ? 0 : count / (totalMs / 1000.0);
|
||||
_report.AppendLine($"\n ▶ {label}");
|
||||
_report.AppendLine($" 总耗时 : {totalMs,10:F3} ms");
|
||||
_report.AppendLine($" 单次 : {perOp,10:F4} μs/op");
|
||||
_report.AppendLine($" 吞吐量 : {throughput,10:F0} ops/s");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9619a4dc431b4b2c8268c32a9e56f72f
|
||||
timeCreated: 1772913062
|
||||
@@ -1,53 +0,0 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class EventBusStaticRegistryAttribute : Attribute
|
||||
{
|
||||
public EventBusStaticRegistryAttribute(params Type[] subscriberTypes)
|
||||
{
|
||||
SubscriberTypes = subscriberTypes ?? Array.Empty<Type>();
|
||||
}
|
||||
|
||||
public Type[] SubscriberTypes { get; }
|
||||
}
|
||||
|
||||
internal static class EventBusGeneratedRegistry
|
||||
{
|
||||
public static IReadOnlyList<Type> GetStaticSubscriberTypes()
|
||||
{
|
||||
var types = new List<Type>();
|
||||
var seen = new HashSet<Type>();
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
object[] attributes;
|
||||
try
|
||||
{
|
||||
attributes = assembly.GetCustomAttributes(typeof(EventBusStaticRegistryAttribute), false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var attribute in attributes.OfType<EventBusStaticRegistryAttribute>())
|
||||
{
|
||||
foreach (var subscriberType in attribute.SubscriberTypes ?? Array.Empty<Type>())
|
||||
{
|
||||
if (subscriberType == null || !seen.Add(subscriberType))
|
||||
continue;
|
||||
|
||||
types.Add(subscriberType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return types;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal static class EventBusRegHelper
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void RegStaticEventHandler(ShrinkEventBusInstance bus)
|
||||
{
|
||||
if (bus == null)
|
||||
throw new ArgumentNullException(nameof(bus));
|
||||
|
||||
RegisterStaticSubscriberTypes(bus, EventBusGeneratedRegistry.GetStaticSubscriberTypes());
|
||||
}
|
||||
|
||||
public static void RegisterStaticSubscriberTypes(ShrinkEventBusInstance bus, IEnumerable<Type> subscriberTypes)
|
||||
{
|
||||
if (bus == null)
|
||||
throw new ArgumentNullException(nameof(bus));
|
||||
if (subscriberTypes == null)
|
||||
throw new ArgumentNullException(nameof(subscriberTypes));
|
||||
|
||||
foreach (var type in subscriberTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (type == null || type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
|
||||
continue;
|
||||
|
||||
RegisterTarget(bus, type, requireSubscriberAttribute: true, lenientWhenNoMethods: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_ = ex;
|
||||
#if UNITY_EDITOR
|
||||
Debug.LogWarning($"[EventBus] Static registration failed for {type?.FullName}: {ex.Message}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void RegisterTarget(ShrinkEventBusInstance bus, object target, bool requireSubscriberAttribute,
|
||||
bool lenientWhenNoMethods = false)
|
||||
{
|
||||
if (bus == null)
|
||||
throw new ArgumentNullException(nameof(bus));
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
switch (target)
|
||||
{
|
||||
case MethodInfo method:
|
||||
RegisterMethod(bus, null, method, requireSubscriberAttribute: false, isStaticRegistration: true);
|
||||
return;
|
||||
case Type staticType:
|
||||
RegisterDeclaredMethods(bus, null, staticType, requireSubscriberAttribute,
|
||||
isStaticRegistration: true, lenientWhenNoMethods);
|
||||
return;
|
||||
default:
|
||||
RegisterDeclaredMethods(bus, target, target.GetType(), requireSubscriberAttribute,
|
||||
isStaticRegistration: false, lenientWhenNoMethods);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterDeclaredMethods(ShrinkEventBusInstance bus, object? target, Type type,
|
||||
bool requireSubscriberAttribute, bool isStaticRegistration, bool lenientWhenNoMethods)
|
||||
{
|
||||
if (requireSubscriberAttribute &&
|
||||
type.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Type {type.FullName} must declare [EventBusSubscriber] before it can be auto-registered.");
|
||||
}
|
||||
|
||||
var flags = (isStaticRegistration ? BindingFlags.Static : BindingFlags.Instance) |
|
||||
BindingFlags.Public | BindingFlags.NonPublic;
|
||||
var methods = type.GetMethods(flags);
|
||||
var foundMethods = 0;
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var hasSubscribeAttribute = method.GetCustomAttributes(typeof(EventSubscribeAttribute), false).Length > 0;
|
||||
if (!hasSubscribeAttribute)
|
||||
continue;
|
||||
|
||||
if (method.IsStatic != isStaticRegistration)
|
||||
{
|
||||
var expected = isStaticRegistration ? "static" : "instance";
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode.");
|
||||
}
|
||||
|
||||
RegisterMethod(bus, target, method, requireSubscriberAttribute: false, isStaticRegistration);
|
||||
foundMethods++;
|
||||
}
|
||||
|
||||
if (foundMethods == 0)
|
||||
{
|
||||
var message =
|
||||
$"Type {type.FullName} has no [EventSubscribe] methods for {(isStaticRegistration ? "static" : "instance")} registration.";
|
||||
if (lenientWhenNoMethods)
|
||||
{
|
||||
Debug.LogWarning($"[EventBus] {message} Auto-registration skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterMethod(ShrinkEventBusInstance bus, object? target, MethodInfo method,
|
||||
bool requireSubscriberAttribute, bool isStaticRegistration)
|
||||
{
|
||||
if (method == null)
|
||||
throw new ArgumentNullException(nameof(method));
|
||||
|
||||
if (!method.IsDefined(typeof(EventSubscribeAttribute), false))
|
||||
throw new InvalidOperationException($"Method {method} is not annotated with [EventSubscribe].");
|
||||
|
||||
if (method.IsStatic != isStaticRegistration)
|
||||
{
|
||||
var expected = isStaticRegistration ? "static" : "instance";
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method} is annotated with [EventSubscribe] but does not match the expected {expected} registration mode.");
|
||||
}
|
||||
|
||||
if (requireSubscriberAttribute && method.DeclaringType != null &&
|
||||
method.DeclaringType.GetCustomAttributes(typeof(EventBusSubscriberAttribute), false).Length == 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Type {method.DeclaringType.FullName} must declare [EventBusSubscriber] before it can be auto-registered.");
|
||||
}
|
||||
|
||||
var subscribeAttr = (EventSubscribeAttribute)method.GetCustomAttributes(typeof(EventSubscribeAttribute), false)[0];
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length != 1)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method} has [EventSubscribe] but declares {parameters.Length} parameters. Event handlers must declare exactly one EventBase parameter.");
|
||||
}
|
||||
|
||||
var parameterType = parameters[0].ParameterType;
|
||||
if (!typeof(EventBase).IsAssignableFrom(parameterType))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method} has [EventSubscribe] but parameter {parameterType.FullName} does not inherit from EventBase.");
|
||||
}
|
||||
|
||||
ProcessMethodRegistration(bus, target, method, subscribeAttr, parameterType);
|
||||
}
|
||||
|
||||
private static void ProcessMethodRegistration(ShrinkEventBusInstance bus, object? target, MethodInfo method,
|
||||
EventSubscribeAttribute subscribeAttr, Type parameterType)
|
||||
{
|
||||
string scope = target == null ? "Static" : "Instance";
|
||||
string typeName = target == null ? method.DeclaringType?.Name ?? "Unknown" : target.GetType().Name;
|
||||
|
||||
if (method.ReturnType == typeof(UniTask))
|
||||
{
|
||||
var funcType = typeof(Func<,>).MakeGenericType(parameterType, typeof(UniTask));
|
||||
var handlerDelegate = target == null
|
||||
? Delegate.CreateDelegate(funcType, method)
|
||||
: Delegate.CreateDelegate(funcType, target, method);
|
||||
bus.RegisterEventInternal(parameterType, handlerDelegate, subscribeAttr.Priority,
|
||||
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
|
||||
$"{scope} {typeName}.{method.Name} (UniTask)", method);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method.ReturnType == typeof(void))
|
||||
{
|
||||
var actionType = typeof(Action<>).MakeGenericType(parameterType);
|
||||
var actionDelegate = target == null
|
||||
? Delegate.CreateDelegate(actionType, method)
|
||||
: Delegate.CreateDelegate(actionType, target, method);
|
||||
bus.RegisterEventInternal(parameterType, actionDelegate, subscribeAttr.Priority,
|
||||
subscribeAttr.NumericPriority, subscribeAttr.ReceiveCanceled,
|
||||
$"{scope} {typeName}.{method.Name} (Sync)", method);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Method {method} has [EventSubscribe] but return type {method.ReturnType.FullName} is unsupported. Use void or UniTask.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a237a407a3a42394eba27bc8e4dbdce2
|
||||
@@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal static class EventCloneUtility
|
||||
{
|
||||
private static readonly object CacheLock = new();
|
||||
private static readonly Dictionary<Type, FieldInfo[]> FieldCache = new();
|
||||
|
||||
public static TEvent CloneForDetachedDispatch<TEvent>(TEvent source) where TEvent : EventBase
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
if (Activator.CreateInstance(source.GetType()) is not TEvent clone)
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot clone event type {source.GetType().FullName}. A public parameterless constructor is required.");
|
||||
|
||||
// 先固化 EventId,让克隆与原事件共享同一个派发标识
|
||||
_ = source.EventId;
|
||||
CopyFields(source, clone);
|
||||
clone.ReleaseAction = null;
|
||||
clone.IsInPool = false;
|
||||
return clone;
|
||||
}
|
||||
|
||||
private static void CopyFields(EventBase source, EventBase target)
|
||||
{
|
||||
var fields = GetCopyableFields(source.GetType());
|
||||
for (var i = 0; i < fields.Length; i++)
|
||||
fields[i].SetValue(target, fields[i].GetValue(source));
|
||||
}
|
||||
|
||||
private static FieldInfo[] GetCopyableFields(Type type)
|
||||
{
|
||||
lock (CacheLock)
|
||||
{
|
||||
if (FieldCache.TryGetValue(type, out var cached))
|
||||
return cached;
|
||||
|
||||
var fields = new List<FieldInfo>();
|
||||
var currentType = type;
|
||||
while (currentType != null && currentType != typeof(object))
|
||||
{
|
||||
var declaredFields = currentType.GetFields(BindingFlags.Instance | BindingFlags.Public |
|
||||
BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
|
||||
for (var i = 0; i < declaredFields.Length; i++)
|
||||
{
|
||||
var field = declaredFields[i];
|
||||
if (field.IsStatic)
|
||||
continue;
|
||||
if (ShouldSkipField(field))
|
||||
continue;
|
||||
|
||||
fields.Add(field);
|
||||
}
|
||||
|
||||
currentType = currentType.BaseType;
|
||||
}
|
||||
|
||||
cached = fields.ToArray();
|
||||
FieldCache[type] = cached;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldSkipField(FieldInfo field)
|
||||
{
|
||||
return field.Name is "<ReleaseAction>k__BackingField"
|
||||
or "<IsInPool>k__BackingField";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using Cysharp.Threading.Tasks;
|
||||
#pragma warning disable CS8632 // 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
|
||||
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public interface IMethodWrapper
|
||||
{
|
||||
MethodInfo OriginalMethod { get; }
|
||||
}
|
||||
|
||||
public class EventHandlerInfo
|
||||
{
|
||||
public long SubscriptionId { get; }
|
||||
public Delegate Handler { get; }
|
||||
internal Action<EventBase>? SyncInvoker { get; }
|
||||
internal Func<EventBase, UniTask>? AsyncInvoker { get; }
|
||||
public EventPriority Priority { get; }
|
||||
public int NumericPriority { get; }
|
||||
public bool ReceiveCanceled { get; }
|
||||
public object? Target { get; }
|
||||
public MethodInfo Method { get; }
|
||||
public string DebugInfo { get; }
|
||||
public Type DeclaringType { get; }
|
||||
public string MethodName { get; }
|
||||
public MethodInfo? OriginalMethod { get; }
|
||||
public string OriginalMethodName { get; }
|
||||
public Type? OriginalDeclaringType { get; }
|
||||
public DateTime RegisteredAtUtc { get; }
|
||||
|
||||
private EventHandlerInfo(long subscriptionId, Delegate handler, Action<EventBase>? syncInvoker,
|
||||
Func<EventBase, UniTask>? asyncInvoker,
|
||||
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
|
||||
MethodInfo? originalMethod = null)
|
||||
{
|
||||
SubscriptionId = subscriptionId;
|
||||
Handler = handler;
|
||||
SyncInvoker = syncInvoker;
|
||||
AsyncInvoker = asyncInvoker;
|
||||
Priority = priority;
|
||||
NumericPriority = numericPriority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
Target = handler.Target;
|
||||
Method = handler.Method;
|
||||
DebugInfo = debugInfo;
|
||||
DeclaringType = Method.DeclaringType ?? typeof(object);
|
||||
MethodName = Method.Name;
|
||||
|
||||
OriginalMethod = ExtractOriginalMethodFromWrapper(handler) ?? originalMethod;
|
||||
|
||||
if (OriginalMethod != null)
|
||||
{
|
||||
OriginalMethodName = OriginalMethod.Name;
|
||||
OriginalDeclaringType = OriginalMethod.DeclaringType;
|
||||
}
|
||||
else
|
||||
{
|
||||
OriginalMethodName = MethodName;
|
||||
OriginalDeclaringType = DeclaringType;
|
||||
}
|
||||
|
||||
RegisteredAtUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private static MethodInfo? ExtractOriginalMethodFromWrapper(Delegate handler)
|
||||
{
|
||||
if (handler.Target is IMethodWrapper wrapper)
|
||||
{
|
||||
return wrapper.OriginalMethod;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string DisplayMethodName => OriginalMethodName;
|
||||
public Type DisplayDeclaringType => OriginalDeclaringType ?? DeclaringType;
|
||||
|
||||
public bool MatchesMethod(MethodInfo method)
|
||||
{
|
||||
return method != null && (ReferenceEquals(Method, method) || ReferenceEquals(OriginalMethod, method));
|
||||
}
|
||||
|
||||
public bool MatchesDeclaringType(Type declaringType)
|
||||
{
|
||||
return declaringType != null &&
|
||||
(DeclaringType == declaringType || OriginalDeclaringType == declaringType);
|
||||
}
|
||||
|
||||
public static EventHandlerInfo Create(long subscriptionId, Delegate handler, Type eventType,
|
||||
EventPriority priority, int numericPriority, bool receiveCanceled, string debugInfo = "",
|
||||
MethodInfo? originalMethod = null)
|
||||
{
|
||||
if (handler == null)
|
||||
throw new System.ArgumentNullException(nameof(handler));
|
||||
if (eventType == null)
|
||||
throw new System.ArgumentNullException(nameof(eventType));
|
||||
|
||||
Action<EventBase>? syncInvoker = null;
|
||||
Func<EventBase, UniTask>? asyncInvoker = null;
|
||||
if (handler.Method.ReturnType == typeof(void))
|
||||
syncInvoker = CreateSyncInvoker(eventType, handler);
|
||||
else if (handler.Method.ReturnType == typeof(UniTask))
|
||||
asyncInvoker = CreateAsyncInvoker(eventType, handler);
|
||||
else
|
||||
throw new System.ArgumentException(
|
||||
$"Unsupported event handler return type {handler.Method.ReturnType.FullName} for {handler.Method}.");
|
||||
|
||||
return new EventHandlerInfo(subscriptionId, handler, syncInvoker, asyncInvoker, priority, numericPriority,
|
||||
receiveCanceled, debugInfo, originalMethod);
|
||||
}
|
||||
|
||||
private static Action<EventBase> CreateSyncInvoker(Type eventType, Delegate handler)
|
||||
{
|
||||
var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateSyncInvokerGeneric),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType);
|
||||
return (Action<EventBase>)factory.Invoke(null, new object[] { handler })!;
|
||||
}
|
||||
|
||||
private static Func<EventBase, UniTask> CreateAsyncInvoker(Type eventType, Delegate handler)
|
||||
{
|
||||
var factory = typeof(EventHandlerInfo).GetMethod(nameof(CreateAsyncInvokerGeneric),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!.MakeGenericMethod(eventType);
|
||||
return (Func<EventBase, UniTask>)factory.Invoke(null, new object[] { handler })!;
|
||||
}
|
||||
|
||||
private static Action<EventBase> CreateSyncInvokerGeneric<TEvent>(Delegate handler) where TEvent : EventBase
|
||||
{
|
||||
var typedHandler = (Action<TEvent>)handler;
|
||||
return eventArgs => typedHandler((TEvent)eventArgs);
|
||||
}
|
||||
|
||||
private static Func<EventBase, UniTask> CreateAsyncInvokerGeneric<TEvent>(Delegate handler)
|
||||
where TEvent : EventBase
|
||||
{
|
||||
var typedHandler = (Func<TEvent, UniTask>)handler;
|
||||
return eventArgs => typedHandler((TEvent)eventArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 19972a32cbf54c7f94d6214fde5635c2
|
||||
timeCreated: 1760098840
|
||||
@@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public static class EventPool<T> where T : EventBase, new()
|
||||
{
|
||||
private const int MaxPoolSize = 128;
|
||||
|
||||
private static readonly Stack<T> Pool = new(32);
|
||||
private static readonly object Lock = new();
|
||||
private static readonly Action<EventBase> CachedReleaseAction = e => Release((T)e);
|
||||
|
||||
public static T Get()
|
||||
{
|
||||
lock (Lock)
|
||||
{
|
||||
if (Pool.Count > 0)
|
||||
{
|
||||
var evt = Pool.Pop();
|
||||
evt.IsInPool = false;
|
||||
return evt;
|
||||
}
|
||||
}
|
||||
|
||||
var newEvt = new T();
|
||||
newEvt.ReleaseAction = CachedReleaseAction;
|
||||
return newEvt;
|
||||
}
|
||||
|
||||
public static void Release(T evt)
|
||||
{
|
||||
if (evt == null)
|
||||
return;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
if (evt.IsInPool || Pool.Count >= MaxPoolSize)
|
||||
return;
|
||||
|
||||
evt.ResetInternal();
|
||||
evt.IsInPool = true;
|
||||
Pool.Push(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c59d817792dd40f08ffed2f1a8d65cb8
|
||||
timeCreated: 1772891540
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public enum EventPriority
|
||||
{
|
||||
HIGHEST = 0,
|
||||
HIGH = 1,
|
||||
NORMAL = 2,
|
||||
LOW = 3,
|
||||
LOWEST = 4,
|
||||
MONITOR = 5
|
||||
}
|
||||
|
||||
internal static class PriorityHelper
|
||||
{
|
||||
public static EventPriority ConvertToEventPriority(int numericPriority)
|
||||
{
|
||||
return numericPriority switch
|
||||
{
|
||||
>= 100 => EventPriority.HIGHEST,
|
||||
>= 50 => EventPriority.HIGH,
|
||||
>= 0 => EventPriority.NORMAL,
|
||||
>= -50 => EventPriority.LOW,
|
||||
_ => EventPriority.LOWEST
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c77c5178c66948f29a7c87c9ae203004
|
||||
timeCreated: 1760098774
|
||||
@@ -1,20 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public class UnsupportedOperationException : InvalidOperationException
|
||||
{
|
||||
public UnsupportedOperationException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
public UnsupportedOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public UnsupportedOperationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 893fdc450e7a4a8e8e1b675f0ce084a7
|
||||
timeCreated: 1760098825
|
||||
@@ -1,109 +1,23 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public interface IShrinkEventSubscription : IDisposable
|
||||
public interface IShrinkEventBus : IDisposable
|
||||
{
|
||||
long SubscriptionId { get; }
|
||||
bool IsDisposed { get; }
|
||||
}
|
||||
ShrinkBusKey Key { get; }
|
||||
ShrinkBusOptions Options { get; }
|
||||
|
||||
public readonly struct ShrinkEventSubscriptionSnapshot
|
||||
{
|
||||
public ShrinkEventSubscriptionSnapshot(long subscriptionId, Type eventType, EventPriority priority,
|
||||
int numericPriority, bool receiveCanceled, object target, string targetTypeName, string methodName,
|
||||
string debugInfo, DateTime registeredAtUtc)
|
||||
{
|
||||
SubscriptionId = subscriptionId;
|
||||
EventType = eventType ?? throw new ArgumentNullException(nameof(eventType));
|
||||
Priority = priority;
|
||||
NumericPriority = numericPriority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
Target = target;
|
||||
TargetTypeName = targetTypeName ?? string.Empty;
|
||||
MethodName = methodName ?? string.Empty;
|
||||
DebugInfo = debugInfo ?? string.Empty;
|
||||
RegisteredAtUtc = registeredAtUtc;
|
||||
}
|
||||
ShrinkPostResult Post<TEvent>(in TEvent eventData)
|
||||
where TEvent : IShrinkEvent;
|
||||
|
||||
public long SubscriptionId { get; }
|
||||
public Type EventType { get; }
|
||||
public EventPriority Priority { get; }
|
||||
public int NumericPriority { get; }
|
||||
public bool ReceiveCanceled { get; }
|
||||
public object Target { get; }
|
||||
public string TargetTypeName { get; }
|
||||
public string MethodName { get; }
|
||||
public string DebugInfo { get; }
|
||||
public DateTime RegisteredAtUtc { get; }
|
||||
public bool IsStaticHandler => Target == null;
|
||||
}
|
||||
UniTask<ShrinkPostResult> PostAsync<TEvent>(TEvent eventData,
|
||||
CancellationToken cancellationToken = default)
|
||||
where TEvent : IShrinkEvent;
|
||||
|
||||
public interface IShrinkEventBus
|
||||
{
|
||||
event Action<EventBase, Type> OnEventTriggered;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
event Action<EventBase, string, string, EventHandlerInfo[]> OnEventTriggeredForEditor;
|
||||
bool EnableDebugRecord { get; set; }
|
||||
#endif
|
||||
|
||||
bool IsStarted { get; }
|
||||
|
||||
void Start();
|
||||
void AutoRegister(object target);
|
||||
void Register(object target);
|
||||
void Unregister(object target);
|
||||
|
||||
void RegisterEvent<TEvent>(Action<TEvent> handler, EventPriority priority = EventPriority.NORMAL,
|
||||
bool receiveCanceled = false) where TEvent : EventBase;
|
||||
void RegisterEvent<TEvent>(Action<TEvent> handler, int priority) where TEvent : EventBase;
|
||||
void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, EventPriority priority = EventPriority.NORMAL,
|
||||
bool receiveCanceled = false) where TEvent : EventBase;
|
||||
void RegisterEvent<TEvent>(Func<TEvent, UniTask> handler, int priority) where TEvent : EventBase;
|
||||
IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase;
|
||||
IShrinkEventSubscription SubscribeEvent<TEvent>(Action<TEvent> handler, int priority)
|
||||
where TEvent : EventBase;
|
||||
IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler,
|
||||
EventPriority priority = EventPriority.NORMAL, bool receiveCanceled = false) where TEvent : EventBase;
|
||||
IShrinkEventSubscription SubscribeEvent<TEvent>(Func<TEvent, UniTask> handler, int priority)
|
||||
where TEvent : EventBase;
|
||||
|
||||
void UnregisterEvent<TEvent>(Action<TEvent> handler) where TEvent : EventBase;
|
||||
void UnregisterEvent<TEvent>(Func<TEvent, UniTask> handler) where TEvent : EventBase;
|
||||
void ClearAllSubscribersForEvent<TEvent>() where TEvent : EventBase;
|
||||
void UnregisterAllEventsForObject(object targetObject);
|
||||
void UnregisterAllEvents();
|
||||
|
||||
bool TriggerEvent<TEvent>(TEvent eventArgs) where TEvent : EventBase;
|
||||
bool TriggerEvent<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase;
|
||||
UniTask<bool> TriggerEventAsync<TEvent>(TEvent eventArgs) where TEvent : EventBase;
|
||||
UniTask<bool> TriggerEventAsync<TEvent>(EventPriority phase, TEvent eventArgs) where TEvent : EventBase;
|
||||
|
||||
EventHandlerInfo[] GetEventSubscribers<TEvent>() where TEvent : EventBase;
|
||||
ListenerList GetListenerList<TEvent>() where TEvent : EventBase;
|
||||
IReadOnlyDictionary<Type, EventHandlerInfo[]> GetAllSubscribersSnapshot();
|
||||
IReadOnlyList<ShrinkEventSubscriptionSnapshot> GetActiveSubscriptionsSnapshot();
|
||||
|
||||
bool IsInstanceRegistered(object target);
|
||||
int GetRegisteredInstanceCount();
|
||||
int GetRegisteredEventTypeCount();
|
||||
}
|
||||
|
||||
public interface IShrinkEventExceptionHandler
|
||||
{
|
||||
void HandleException(IShrinkEventBus bus, EventBase eventArgs, EventHandlerInfo[] listeners, int index,
|
||||
Exception exception);
|
||||
}
|
||||
|
||||
public enum ShrinkEventExceptionHandlingMode
|
||||
{
|
||||
LogAndContinue,
|
||||
Throw,
|
||||
LogAndThrow
|
||||
IDisposable Attach(object target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public class ListenerList
|
||||
{
|
||||
private static readonly EventPriority[] Priorities = (EventPriority[])Enum.GetValues(typeof(EventPriority));
|
||||
|
||||
private readonly object _lock;
|
||||
private readonly List<EventHandlerInfo>[] _priorityBuckets;
|
||||
private readonly ListenerList? _parent;
|
||||
private List<ListenerList>? _children;
|
||||
private EventHandlerInfo[] _snapshot = Array.Empty<EventHandlerInfo>();
|
||||
private readonly EventHandlerInfo[]?[] _phaseSnapshots = new EventHandlerInfo[Priorities.Length][];
|
||||
private bool _dirty;
|
||||
|
||||
public ListenerList(ListenerList? parent = null) : this(parent, null)
|
||||
{
|
||||
}
|
||||
|
||||
// 同一条父子链必须共用一把锁,否则脏标记传播与快照重建会产生竞态
|
||||
internal ListenerList(ListenerList? parent, object? sharedLock)
|
||||
{
|
||||
_lock = sharedLock ?? parent?._lock ?? new object();
|
||||
_priorityBuckets = new List<EventHandlerInfo>[Priorities.Length];
|
||||
for (var i = 0; i < _priorityBuckets.Length; i++)
|
||||
_priorityBuckets[i] = new List<EventHandlerInfo>();
|
||||
|
||||
_parent = parent;
|
||||
_parent?.AddChild(this);
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
public int LocalCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var count = 0;
|
||||
foreach (var list in _priorityBuckets)
|
||||
count += list.Count;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => GetHandlers().Length;
|
||||
|
||||
public void Add(EventHandlerInfo info)
|
||||
{
|
||||
if (info == null)
|
||||
throw new ArgumentNullException(nameof(info));
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var list = _priorityBuckets[(int)info.Priority];
|
||||
var index = BinarySearchInsertIndex(list, info.NumericPriority);
|
||||
list.Insert(index, info);
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(Delegate handler)
|
||||
{
|
||||
if (handler == null)
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var list in _priorityBuckets)
|
||||
{
|
||||
for (var i = 0; i < list.Count; i++)
|
||||
{
|
||||
if (!list[i].Handler.Equals(handler))
|
||||
continue;
|
||||
|
||||
list.RemoveAt(i);
|
||||
MarkDirty();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public int RemoveWhere(Predicate<EventHandlerInfo> predicate)
|
||||
{
|
||||
if (predicate == null)
|
||||
return 0;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var removedCount = 0;
|
||||
foreach (var list in _priorityBuckets)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!predicate(list[i]))
|
||||
continue;
|
||||
|
||||
list.RemoveAt(i);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (removedCount > 0)
|
||||
MarkDirty();
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveTarget(object target)
|
||||
{
|
||||
if (target == null)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var removed = false;
|
||||
foreach (var list in _priorityBuckets)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (!ReferenceEquals(list[i].Target, target))
|
||||
continue;
|
||||
|
||||
list.RemoveAt(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (removed)
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandlerInfo[] GetHandlers()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_dirty)
|
||||
RebuildSnapshot();
|
||||
|
||||
return _snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public EventHandlerInfo[] GetHandlers(EventPriority priority)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_dirty)
|
||||
RebuildSnapshot();
|
||||
|
||||
return _phaseSnapshots[(int)priority] ?? Array.Empty<EventHandlerInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var list in _priorityBuckets)
|
||||
list.Clear();
|
||||
MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private void AddChild(ListenerList child)
|
||||
{
|
||||
if (child == null)
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_children ??= new List<ListenerList>(2);
|
||||
_children.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkDirty()
|
||||
{
|
||||
_dirty = true;
|
||||
_snapshot = Array.Empty<EventHandlerInfo>();
|
||||
Array.Clear(_phaseSnapshots, 0, _phaseSnapshots.Length);
|
||||
|
||||
if (_children == null)
|
||||
return;
|
||||
|
||||
for (var i = 0; i < _children.Count; i++)
|
||||
_children[i].MarkDirty();
|
||||
}
|
||||
|
||||
private void RebuildSnapshot()
|
||||
{
|
||||
var merged = new List<EventHandlerInfo>();
|
||||
|
||||
for (var i = 0; i < Priorities.Length; i++)
|
||||
{
|
||||
var parentHandlers = _parent != null
|
||||
? _parent.GetHandlers(Priorities[i])
|
||||
: Array.Empty<EventHandlerInfo>();
|
||||
|
||||
var phaseSnapshot = MergeByNumericPriority(_priorityBuckets[i], parentHandlers);
|
||||
_phaseSnapshots[i] = phaseSnapshot;
|
||||
merged.AddRange(phaseSnapshot);
|
||||
}
|
||||
|
||||
_snapshot = merged.ToArray();
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
// 两侧均已按 NumericPriority 降序排列;平局时本类型 handler 在前
|
||||
private static EventHandlerInfo[] MergeByNumericPriority(List<EventHandlerInfo> own,
|
||||
EventHandlerInfo[] parentHandlers)
|
||||
{
|
||||
if (parentHandlers.Length == 0)
|
||||
return own.ToArray();
|
||||
if (own.Count == 0)
|
||||
return parentHandlers;
|
||||
|
||||
var result = new EventHandlerInfo[own.Count + parentHandlers.Length];
|
||||
int i = 0, j = 0, k = 0;
|
||||
while (i < own.Count && j < parentHandlers.Length)
|
||||
{
|
||||
result[k++] = parentHandlers[j].NumericPriority > own[i].NumericPriority
|
||||
? parentHandlers[j++]
|
||||
: own[i++];
|
||||
}
|
||||
|
||||
while (i < own.Count)
|
||||
result[k++] = own[i++];
|
||||
while (j < parentHandlers.Length)
|
||||
result[k++] = parentHandlers[j++];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int BinarySearchInsertIndex(List<EventHandlerInfo> list, int numericPriority)
|
||||
{
|
||||
var lo = 0;
|
||||
var hi = list.Count;
|
||||
|
||||
while (lo < hi)
|
||||
{
|
||||
var mid = (lo + hi) >> 1;
|
||||
var cmp = numericPriority.CompareTo(list[mid].NumericPriority);
|
||||
if (cmp > 0)
|
||||
hi = mid;
|
||||
else
|
||||
lo = mid + 1;
|
||||
}
|
||||
|
||||
return lo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 864eb3e7bf8f443c86472995e408c580
|
||||
timeCreated: 1760098877
|
||||
@@ -0,0 +1,546 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
internal interface IShrinkBusScheduler : IDisposable
|
||||
{
|
||||
bool IsOnSchedulerThread { get; }
|
||||
bool TryPost(Action action);
|
||||
UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken);
|
||||
UniTask ShutdownAsync(bool drain, TimeSpan timeout);
|
||||
}
|
||||
|
||||
internal sealed class ShrinkQueueFullException : InvalidOperationException
|
||||
{
|
||||
public ShrinkQueueFullException(string schedulerName)
|
||||
: base($"Scheduler queue '{schedulerName}' is full.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ShrinkBusSchedulerFactory
|
||||
{
|
||||
public static IShrinkBusScheduler Create(ShrinkBusOptions options, string name)
|
||||
{
|
||||
return options.Scheduler switch
|
||||
{
|
||||
ShrinkBusSchedulerKind.MainThread => new ShrinkMainThreadScheduler(name, options),
|
||||
ShrinkBusSchedulerKind.DedicatedThread => new ShrinkDedicatedThreadScheduler(name, options),
|
||||
ShrinkBusSchedulerKind.TaskPool => new ShrinkTaskPoolScheduler(name, options),
|
||||
_ => new ShrinkInlineScheduler()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShrinkSchedulerWorkItem
|
||||
{
|
||||
private readonly UniTaskCompletionSource? _completion;
|
||||
|
||||
public ShrinkSchedulerWorkItem(Func<UniTask> action, bool awaitable)
|
||||
{
|
||||
Action = action ?? throw new ArgumentNullException(nameof(action));
|
||||
if (awaitable)
|
||||
_completion = new UniTaskCompletionSource();
|
||||
}
|
||||
|
||||
public Func<UniTask> Action { get; }
|
||||
public UniTask Completion => _completion?.Task ?? UniTask.CompletedTask;
|
||||
|
||||
public void Complete() => _completion?.TrySetResult();
|
||||
public void Fail(Exception exception) => _completion?.TrySetException(exception);
|
||||
public void Cancel(CancellationToken cancellationToken) => _completion?.TrySetCanceled(cancellationToken);
|
||||
public void Drop(string schedulerName) =>
|
||||
_completion?.TrySetException(new ShrinkQueueFullException(schedulerName));
|
||||
}
|
||||
|
||||
internal sealed class ShrinkSchedulerQueue : IDisposable
|
||||
{
|
||||
private readonly ConcurrentQueue<ShrinkSchedulerWorkItem> _queue = new();
|
||||
private readonly SemaphoreSlim _slots;
|
||||
private readonly ShrinkQueueOverflowPolicy _overflowPolicy;
|
||||
private readonly string _name;
|
||||
private int _disposed;
|
||||
|
||||
public ShrinkSchedulerQueue(string name, ShrinkBusOptions options)
|
||||
{
|
||||
_name = name;
|
||||
_overflowPolicy = options.OverflowPolicy;
|
||||
_slots = new SemaphoreSlim(options.QueueCapacity, options.QueueCapacity);
|
||||
}
|
||||
|
||||
public bool IsEmpty => _queue.IsEmpty;
|
||||
|
||||
public bool TryEnqueue(ShrinkSchedulerWorkItem item)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
return false;
|
||||
|
||||
if (!_slots.Wait(0))
|
||||
{
|
||||
if (_overflowPolicy != ShrinkQueueOverflowPolicy.DropOldest ||
|
||||
!_queue.TryDequeue(out var dropped))
|
||||
return false;
|
||||
|
||||
dropped.Drop(_name);
|
||||
_slots.Release();
|
||||
if (!_slots.Wait(0))
|
||||
return false;
|
||||
}
|
||||
|
||||
_queue.Enqueue(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async UniTask EnqueueAsync(ShrinkSchedulerWorkItem item, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
throw new ObjectDisposedException(_name);
|
||||
|
||||
if (_overflowPolicy == ShrinkQueueOverflowPolicy.Wait)
|
||||
{
|
||||
await _slots.WaitAsync(cancellationToken).AsUniTask(useCurrentSynchronizationContext: false);
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
{
|
||||
_slots.Release();
|
||||
throw new ObjectDisposedException(_name);
|
||||
}
|
||||
_queue.Enqueue(item);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryEnqueue(item))
|
||||
throw new ShrinkQueueFullException(_name);
|
||||
}
|
||||
|
||||
public bool TryDequeue(out ShrinkSchedulerWorkItem item)
|
||||
{
|
||||
if (!_queue.TryDequeue(out item!))
|
||||
return false;
|
||||
_slots.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void DropPending()
|
||||
{
|
||||
while (_queue.TryDequeue(out var item))
|
||||
{
|
||||
_slots.Release();
|
||||
item.Drop(_name);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
return;
|
||||
DropPending();
|
||||
_slots.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ShrinkSchedulerWorkItemRunner
|
||||
{
|
||||
public static async UniTask RunAsync(ShrinkSchedulerWorkItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
await item.Action();
|
||||
item.Complete();
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
item.Cancel(ex.CancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
item.Fail(ex);
|
||||
ShrinkEventDiagnostics.LogException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void RunBlocking(ShrinkSchedulerWorkItem item)
|
||||
{
|
||||
try
|
||||
{
|
||||
item.Action().GetAwaiter().GetResult();
|
||||
item.Complete();
|
||||
}
|
||||
catch (OperationCanceledException ex)
|
||||
{
|
||||
item.Cancel(ex.CancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
item.Fail(ex);
|
||||
ShrinkEventDiagnostics.LogException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShrinkInlineScheduler : IShrinkBusScheduler
|
||||
{
|
||||
public bool IsOnSchedulerThread => true;
|
||||
|
||||
public bool TryPost(Action action)
|
||||
{
|
||||
action();
|
||||
return true;
|
||||
}
|
||||
|
||||
public UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
return action();
|
||||
}
|
||||
|
||||
public UniTask ShutdownAsync(bool drain, TimeSpan timeout) => UniTask.CompletedTask;
|
||||
public void Dispose() { }
|
||||
}
|
||||
|
||||
internal sealed class ShrinkMainThreadScheduler : IShrinkBusScheduler
|
||||
{
|
||||
private readonly ShrinkSchedulerQueue _queue;
|
||||
private int _pumpScheduled;
|
||||
private int _disposed;
|
||||
|
||||
public ShrinkMainThreadScheduler(string name, ShrinkBusOptions options)
|
||||
{
|
||||
_queue = new ShrinkSchedulerQueue(name, options);
|
||||
}
|
||||
|
||||
public bool IsOnSchedulerThread => PlayerLoopHelper.IsMainThread;
|
||||
|
||||
public bool TryPost(Action action)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
return false;
|
||||
if (IsOnSchedulerThread)
|
||||
{
|
||||
action();
|
||||
return true;
|
||||
}
|
||||
|
||||
var item = new ShrinkSchedulerWorkItem(() =>
|
||||
{
|
||||
action();
|
||||
return UniTask.CompletedTask;
|
||||
}, awaitable: false);
|
||||
if (!_queue.TryEnqueue(item))
|
||||
return false;
|
||||
SchedulePump();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
throw new ObjectDisposedException(nameof(ShrinkMainThreadScheduler));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (IsOnSchedulerThread)
|
||||
{
|
||||
await action();
|
||||
return;
|
||||
}
|
||||
|
||||
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
|
||||
await _queue.EnqueueAsync(item, cancellationToken);
|
||||
SchedulePump();
|
||||
await item.Completion.AttachExternalCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
|
||||
{
|
||||
Interlocked.Exchange(ref _disposed, 1);
|
||||
if (!drain)
|
||||
_queue.DropPending();
|
||||
else
|
||||
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
|
||||
_queue, () => Volatile.Read(ref _pumpScheduled) != 0, timeout);
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
return;
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
private void SchedulePump()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _pumpScheduled, 1) != 0)
|
||||
return;
|
||||
UniTask.Void(PumpAsync);
|
||||
}
|
||||
|
||||
private async UniTaskVoid PumpAsync()
|
||||
{
|
||||
await UniTask.SwitchToMainThread();
|
||||
try
|
||||
{
|
||||
while (_queue.TryDequeue(out var item))
|
||||
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Exchange(ref _pumpScheduled, 0);
|
||||
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
|
||||
SchedulePump();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShrinkTaskPoolScheduler : IShrinkBusScheduler
|
||||
{
|
||||
private readonly ShrinkSchedulerQueue _queue;
|
||||
private readonly int _maxConcurrency;
|
||||
private int _workers;
|
||||
private int _disposed;
|
||||
|
||||
public ShrinkTaskPoolScheduler(string name, ShrinkBusOptions options)
|
||||
{
|
||||
_queue = new ShrinkSchedulerQueue(name, options);
|
||||
_maxConcurrency = options.DispatchMode == ShrinkDispatchMode.Ordered ? 1 : options.MaxConcurrency;
|
||||
}
|
||||
|
||||
public bool IsOnSchedulerThread => false;
|
||||
|
||||
public bool TryPost(Action action)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
return false;
|
||||
var item = new ShrinkSchedulerWorkItem(() =>
|
||||
{
|
||||
action();
|
||||
return UniTask.CompletedTask;
|
||||
}, awaitable: false);
|
||||
if (!_queue.TryEnqueue(item))
|
||||
return false;
|
||||
ScheduleWorkers();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
throw new ObjectDisposedException(nameof(ShrinkTaskPoolScheduler));
|
||||
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
|
||||
await _queue.EnqueueAsync(item, cancellationToken);
|
||||
ScheduleWorkers();
|
||||
await item.Completion.AttachExternalCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
|
||||
{
|
||||
Interlocked.Exchange(ref _disposed, 1);
|
||||
if (!drain)
|
||||
_queue.DropPending();
|
||||
else
|
||||
await ShrinkSchedulerShutdown.WaitUntilDrainedAsync(
|
||||
_queue, () => Volatile.Read(ref _workers) != 0, timeout);
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
return;
|
||||
_queue.Dispose();
|
||||
}
|
||||
|
||||
private void ScheduleWorkers()
|
||||
{
|
||||
while (!_queue.IsEmpty)
|
||||
{
|
||||
var current = Volatile.Read(ref _workers);
|
||||
if (current >= _maxConcurrency ||
|
||||
Interlocked.CompareExchange(ref _workers, current + 1, current) != current)
|
||||
return;
|
||||
UniTask.Void(WorkerAsync);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTaskVoid WorkerAsync()
|
||||
{
|
||||
await UniTask.SwitchToThreadPool();
|
||||
try
|
||||
{
|
||||
while (_queue.TryDequeue(out var item))
|
||||
await ShrinkSchedulerWorkItemRunner.RunAsync(item);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Interlocked.Decrement(ref _workers);
|
||||
if (!_queue.IsEmpty && Volatile.Read(ref _disposed) == 0)
|
||||
ScheduleWorkers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShrinkDedicatedThreadScheduler : IShrinkBusScheduler
|
||||
{
|
||||
private readonly ShrinkSchedulerQueue _queue;
|
||||
private readonly AutoResetEvent _signal = new(false);
|
||||
private readonly ManualResetEventSlim _stopped = new(false);
|
||||
private readonly Thread _thread;
|
||||
private int _disposed;
|
||||
private int _threadId;
|
||||
private int _shutdownTimedOut;
|
||||
private int _waitHandlesDisposed;
|
||||
|
||||
public ShrinkDedicatedThreadScheduler(string name, ShrinkBusOptions options)
|
||||
{
|
||||
_queue = new ShrinkSchedulerQueue(name, options);
|
||||
_thread = new Thread(Run)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"ShrinkBus:{name}"
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
public bool IsOnSchedulerThread => Thread.CurrentThread.ManagedThreadId == Volatile.Read(ref _threadId);
|
||||
|
||||
public bool TryPost(Action action)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
return false;
|
||||
if (IsOnSchedulerThread)
|
||||
{
|
||||
action();
|
||||
return true;
|
||||
}
|
||||
|
||||
var item = new ShrinkSchedulerWorkItem(() =>
|
||||
{
|
||||
action();
|
||||
return UniTask.CompletedTask;
|
||||
}, awaitable: false);
|
||||
if (!_queue.TryEnqueue(item))
|
||||
return false;
|
||||
_signal.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async UniTask PostAsync(Func<UniTask> action, CancellationToken cancellationToken)
|
||||
{
|
||||
if (Volatile.Read(ref _disposed) != 0)
|
||||
throw new ObjectDisposedException(nameof(ShrinkDedicatedThreadScheduler));
|
||||
if (IsOnSchedulerThread)
|
||||
{
|
||||
await action();
|
||||
return;
|
||||
}
|
||||
|
||||
var item = new ShrinkSchedulerWorkItem(action, awaitable: true);
|
||||
await _queue.EnqueueAsync(item, cancellationToken);
|
||||
_signal.Set();
|
||||
await item.Completion.AttachExternalCancellation(cancellationToken);
|
||||
}
|
||||
|
||||
public async UniTask ShutdownAsync(bool drain, TimeSpan timeout)
|
||||
{
|
||||
Interlocked.Exchange(ref _disposed, 1);
|
||||
if (!drain)
|
||||
_queue.DropPending();
|
||||
_signal.Set();
|
||||
var waitMs = timeout == Timeout.InfiniteTimeSpan
|
||||
? Timeout.Infinite
|
||||
: Math.Max(0, (int)Math.Min(int.MaxValue, timeout.TotalMilliseconds));
|
||||
var stopped = await UniTask.RunOnThreadPool(() => _stopped.Wait(waitMs));
|
||||
if (!stopped)
|
||||
{
|
||||
_queue.DropPending();
|
||||
Volatile.Write(ref _shutdownTimedOut, 1);
|
||||
try
|
||||
{
|
||||
if (_stopped.IsSet)
|
||||
DisposeWaitHandles();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
return;
|
||||
}
|
||||
DisposeWaitHandles();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||
return;
|
||||
_queue.DropPending();
|
||||
Volatile.Write(ref _shutdownTimedOut, 1);
|
||||
_signal.Set();
|
||||
}
|
||||
|
||||
private void Run()
|
||||
{
|
||||
Volatile.Write(ref _threadId, Thread.CurrentThread.ManagedThreadId);
|
||||
try
|
||||
{
|
||||
while (Volatile.Read(ref _disposed) == 0 || !_queue.IsEmpty)
|
||||
{
|
||||
if (!_queue.TryDequeue(out var item))
|
||||
{
|
||||
_signal.WaitOne(50);
|
||||
continue;
|
||||
}
|
||||
ShrinkSchedulerWorkItemRunner.RunBlocking(item);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stopped.Set();
|
||||
if (Volatile.Read(ref _shutdownTimedOut) != 0)
|
||||
DisposeWaitHandles();
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeWaitHandles()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _waitHandlesDisposed, 1) != 0)
|
||||
return;
|
||||
_queue.Dispose();
|
||||
_signal.Dispose();
|
||||
_stopped.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ShrinkEventDiagnostics
|
||||
{
|
||||
public static void LogException(Exception exception)
|
||||
{
|
||||
#if UNITY_5_3_OR_NEWER
|
||||
UnityEngine.Debug.LogException(exception);
|
||||
#else
|
||||
Trace.TraceError(exception.ToString());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
internal static class ShrinkSchedulerShutdown
|
||||
{
|
||||
public static async UniTask WaitUntilDrainedAsync(ShrinkSchedulerQueue queue,
|
||||
Func<bool> hasActiveWork, TimeSpan timeout)
|
||||
{
|
||||
var started = Stopwatch.StartNew();
|
||||
while (!queue.IsEmpty || hasActiveWork())
|
||||
{
|
||||
if (timeout != Timeout.InfiniteTimeSpan && started.Elapsed >= timeout)
|
||||
{
|
||||
queue.DropPending();
|
||||
return;
|
||||
}
|
||||
await UniTask.Delay(1, ignoreTimeScale: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04809187aa0ce3b43a4df12b048a1c98
|
||||
guid: 8d5885f09c9a4de89cd95de44433bb17
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class ShrinkEventSubscriberAttribute : Attribute
|
||||
{
|
||||
public string OwnerId { get; set; } = string.Empty;
|
||||
public string DefaultBus { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class ShrinkSubscribeAttribute : Attribute
|
||||
{
|
||||
public string Bus { get; set; } = string.Empty;
|
||||
public ShrinkEventPriority Priority { get; set; } = ShrinkEventPriority.Normal;
|
||||
public int NumericPriority { get; set; }
|
||||
public bool ReceiveCanceled { get; set; }
|
||||
|
||||
public ShrinkSubscribeAttribute() { }
|
||||
|
||||
public ShrinkSubscribeAttribute(ShrinkEventPriority priority, bool receiveCanceled = false)
|
||||
{
|
||||
Priority = priority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
}
|
||||
|
||||
public ShrinkSubscribeAttribute(int priority)
|
||||
{
|
||||
NumericPriority = priority;
|
||||
Priority = priority switch
|
||||
{
|
||||
>= 100 => ShrinkEventPriority.Highest,
|
||||
>= 50 => ShrinkEventPriority.High,
|
||||
>= 0 => ShrinkEventPriority.Normal,
|
||||
>= -50 => ShrinkEventPriority.Low,
|
||||
_ => ShrinkEventPriority.Lowest
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false)]
|
||||
public sealed class ShrinkEventAttribute : Attribute
|
||||
{
|
||||
public ShrinkDispatchMode Dispatch { get; set; } = ShrinkDispatchMode.Ordered;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 876564c111495b2489870b29ab75da5e
|
||||
guid: 32e8ae06929942288c57cd8c89b3d3a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,293 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public interface IShrinkBusResolver
|
||||
{
|
||||
IShrinkEventBus GetBus(ShrinkBusKey key);
|
||||
bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus);
|
||||
}
|
||||
|
||||
public interface IShrinkGeneratedSubscriber
|
||||
{
|
||||
IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null);
|
||||
}
|
||||
|
||||
public sealed class ShrinkEventBinding : IDisposable
|
||||
{
|
||||
private readonly List<IDisposable> _items = new();
|
||||
private bool _disposed;
|
||||
|
||||
public void Add(IDisposable subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
if (_disposed)
|
||||
{
|
||||
subscription.Dispose();
|
||||
throw new ObjectDisposedException(nameof(ShrinkEventBinding));
|
||||
}
|
||||
_items.Add(subscription);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
for (var i = _items.Count - 1; i >= 0; i--)
|
||||
_items[i].Dispose();
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShrinkStaticBindingRegistry
|
||||
{
|
||||
private sealed class Entry
|
||||
{
|
||||
public long Id;
|
||||
public ShrinkBusKey Key;
|
||||
public Func<IShrinkBusResolver, IDisposable> Factory = null!;
|
||||
}
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly List<Entry> Entries = new();
|
||||
private static readonly Dictionary<long, IDisposable> Bindings = new();
|
||||
private static long _nextId;
|
||||
|
||||
public static void Register(ShrinkBusKey key,
|
||||
Func<IShrinkBusResolver, IDisposable> factory)
|
||||
{
|
||||
if (factory == null)
|
||||
throw new ArgumentNullException(nameof(factory));
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
var entry = new Entry
|
||||
{
|
||||
Id = Interlocked.Increment(ref _nextId),
|
||||
Key = key,
|
||||
Factory = factory
|
||||
};
|
||||
Entries.Add(entry);
|
||||
TryAttachLocked(entry, EventBus.Resolver);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Register<TEvent>(string bus, Action<TEvent> handler,
|
||||
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
|
||||
where TEvent : IShrinkEvent
|
||||
{
|
||||
var key = ShrinkBusKey.Parse(bus);
|
||||
Register(key, resolver => ShrinkGeneratedBinding.Subscribe(
|
||||
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
|
||||
}
|
||||
|
||||
public static void RegisterAsync<TEvent>(string bus, ShrinkAsyncEventHandler<TEvent> handler,
|
||||
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
|
||||
where TEvent : IShrinkEvent
|
||||
{
|
||||
var key = ShrinkBusKey.Parse(bus);
|
||||
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsync(
|
||||
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
|
||||
}
|
||||
|
||||
public static void RegisterAsyncLegacy<TEvent>(string bus, Func<TEvent, UniTask> handler,
|
||||
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled)
|
||||
where TEvent : IShrinkEvent
|
||||
{
|
||||
var key = ShrinkBusKey.Parse(bus);
|
||||
Register(key, resolver => ShrinkGeneratedBinding.SubscribeAsyncLegacy(
|
||||
resolver, null, bus, null, handler, priority, numericPriority, receiveCanceled));
|
||||
}
|
||||
|
||||
internal static void AttachForBus(ShrinkBusKey key, IShrinkBusResolver resolver)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
for (var i = 0; i < Entries.Count; i++)
|
||||
{
|
||||
if (Entries[i].Key == key)
|
||||
TryAttachLocked(Entries[i], resolver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void DetachBus(ShrinkBusKey key)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
for (var i = 0; i < Entries.Count; i++)
|
||||
{
|
||||
var entry = Entries[i];
|
||||
if (entry.Key != key || !Bindings.TryGetValue(entry.Id, out var binding))
|
||||
continue;
|
||||
Bindings.Remove(entry.Id);
|
||||
binding.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryAttachLocked(Entry entry, IShrinkBusResolver resolver)
|
||||
{
|
||||
if (Bindings.ContainsKey(entry.Id) || !resolver.TryGetBus(entry.Key, out _))
|
||||
return;
|
||||
Bindings.Add(entry.Id, entry.Factory(resolver));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ShrinkEventBusHost : IShrinkBusResolver, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<ShrinkBusKey, IShrinkEventBus> _buses = new();
|
||||
private bool _disposed;
|
||||
|
||||
public IEnumerable<IShrinkEventBus> Buses => _buses.Values;
|
||||
|
||||
public IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
var bus = new ShrinkEventBusBuilder()
|
||||
.WithKey(key)
|
||||
.WithOptions(options)
|
||||
.Build();
|
||||
if (!_buses.TryAdd(key, bus))
|
||||
{
|
||||
if (bus is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
throw new InvalidOperationException($"Bus '{key}' is already registered.");
|
||||
}
|
||||
return bus;
|
||||
}
|
||||
|
||||
public IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, Func<ShrinkBusOptions> optionsFactory)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(ShrinkEventBusHost));
|
||||
if (optionsFactory == null)
|
||||
throw new ArgumentNullException(nameof(optionsFactory));
|
||||
|
||||
return _buses.GetOrAdd(key, busKey =>
|
||||
new ShrinkEventBusBuilder()
|
||||
.WithKey(busKey)
|
||||
.WithOptions(optionsFactory())
|
||||
.Build());
|
||||
}
|
||||
|
||||
public IShrinkEventBus GetBus(ShrinkBusKey key)
|
||||
{
|
||||
if (_buses.TryGetValue(key, out var bus))
|
||||
return bus;
|
||||
throw new KeyNotFoundException($"Bus '{key}' is not registered.");
|
||||
}
|
||||
|
||||
public bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) => _buses.TryGetValue(key, out bus!);
|
||||
|
||||
public IDisposable Attach(object target, ShrinkBusKey? defaultBus = null)
|
||||
{
|
||||
if (target == null)
|
||||
throw new ArgumentNullException(nameof(target));
|
||||
|
||||
if (target is IShrinkGeneratedSubscriber generated)
|
||||
return generated.AttachGenerated(this, defaultBus);
|
||||
|
||||
var bus = GetBus(defaultBus ?? ShrinkBusKey.Game);
|
||||
return bus.Attach(target);
|
||||
}
|
||||
|
||||
public bool RemoveBus(ShrinkBusKey key)
|
||||
{
|
||||
if (!_buses.TryRemove(key, out var bus))
|
||||
return false;
|
||||
if (bus is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
foreach (var bus in _buses.Values)
|
||||
{
|
||||
if (bus is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
}
|
||||
_buses.Clear();
|
||||
}
|
||||
|
||||
public async UniTask ShutdownAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_disposed = true;
|
||||
foreach (var bus in _buses.Values)
|
||||
{
|
||||
if (bus is ShrinkEventBusInstance instance)
|
||||
await instance.ShutdownAsync();
|
||||
else
|
||||
bus.Dispose();
|
||||
}
|
||||
_buses.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShrinkGeneratedBinding
|
||||
{
|
||||
public static IShrinkBusResolver RuntimeResolver => EventBus.Resolver;
|
||||
|
||||
public static IDisposable Subscribe<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||
string? configuredBus, object? owner, Action<TEvent> handler, ShrinkEventPriority priority,
|
||||
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||
{
|
||||
var bus = ResolveBus(resolver, defaultBus, configuredBus);
|
||||
if (bus is not ShrinkEventBusInstance instance)
|
||||
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
|
||||
return instance.SubscribeGenerated(handler,
|
||||
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeAsync<TEvent>(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||
string? configuredBus, object? owner, ShrinkAsyncEventHandler<TEvent> handler,
|
||||
ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||
{
|
||||
var bus = ResolveBus(resolver, defaultBus, configuredBus);
|
||||
if (bus is not ShrinkEventBusInstance instance)
|
||||
throw new InvalidOperationException("Generated bindings require the built-in ShrinkEventBus implementation.");
|
||||
return instance.SubscribeGenerated(handler,
|
||||
new ShrinkSubscribeDescriptor(configuredBus, priority, numericPriority, receiveCanceled));
|
||||
}
|
||||
|
||||
public static IDisposable SubscribeAsyncLegacy<TEvent>(IShrinkBusResolver resolver,
|
||||
ShrinkBusKey? defaultBus, string? configuredBus, object? owner,
|
||||
Func<TEvent, UniTask> handler, ShrinkEventPriority priority,
|
||||
int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent
|
||||
{
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException(nameof(handler));
|
||||
return SubscribeAsync<TEvent>(resolver, defaultBus, configuredBus, owner,
|
||||
(eventData, _) => handler(eventData), priority, numericPriority, receiveCanceled);
|
||||
}
|
||||
|
||||
private static IShrinkEventBus ResolveBus(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus,
|
||||
string? configuredBus)
|
||||
{
|
||||
if (resolver == null)
|
||||
throw new ArgumentNullException(nameof(resolver));
|
||||
var key = !string.IsNullOrWhiteSpace(configuredBus)
|
||||
? ShrinkBusKey.Parse(configuredBus)
|
||||
: defaultBus ?? ShrinkBusKey.Game;
|
||||
return resolver.GetBus(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 11b7dddad0d215b47a7249e74a574105
|
||||
guid: f19286886fb64b3ba7bfbd010c540913
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -1,76 +1,36 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public sealed class ShrinkEventBusBuilder
|
||||
{
|
||||
internal IShrinkEventExceptionHandler? ExceptionHandler { get; private set; }
|
||||
internal ShrinkEventExceptionHandlingMode ExceptionHandlingMode { get; private set; } =
|
||||
ShrinkEventExceptionHandlingMode.LogAndContinue;
|
||||
internal Action<Type>? EventClassChecker { get; private set; }
|
||||
internal bool StartShutdownEnabled { get; private set; }
|
||||
internal bool CheckTypesOnDispatchEnabled { get; private set; }
|
||||
internal bool AllowPerPhaseDispatchEnabled { get; private set; }
|
||||
internal ShrinkBusKey Key { get; private set; } = ShrinkBusKey.Game;
|
||||
internal ShrinkBusOptions Options { get; private set; } = ShrinkBusOptions.Inline();
|
||||
internal Action<IShrinkEvent, Type, ShrinkBusKey>? PostObserver { get; private set; }
|
||||
|
||||
public ShrinkEventBusBuilder SetExceptionHandler(IShrinkEventExceptionHandler handler)
|
||||
public ShrinkEventBusBuilder WithKey(ShrinkBusKey key)
|
||||
{
|
||||
ExceptionHandler = handler ?? throw new ArgumentNullException(nameof(handler));
|
||||
Key = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShrinkEventBusBuilder SetExceptionHandlingMode(ShrinkEventExceptionHandlingMode mode)
|
||||
public ShrinkEventBusBuilder WithOptions(ShrinkBusOptions options)
|
||||
{
|
||||
ExceptionHandlingMode = mode;
|
||||
Options = (options ?? throw new ArgumentNullException(nameof(options))).CloneValidated();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShrinkEventBusBuilder StartShutdown()
|
||||
public IShrinkEventBus Build() => Options.Scheduler == ShrinkBusSchedulerKind.Inline
|
||||
? new ShrinkInlineEventBusInstance(this)
|
||||
: new ShrinkEventBusInstance(this);
|
||||
|
||||
internal ShrinkEventBusBuilder WithPostObserver(
|
||||
Action<IShrinkEvent, Type, ShrinkBusKey> observer)
|
||||
{
|
||||
StartShutdownEnabled = true;
|
||||
PostObserver = observer ?? throw new ArgumentNullException(nameof(observer));
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShrinkEventBusBuilder CheckTypesOnDispatch()
|
||||
{
|
||||
CheckTypesOnDispatchEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShrinkEventBusBuilder AllowPerPhaseDispatch()
|
||||
{
|
||||
AllowPerPhaseDispatchEnabled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShrinkEventBusBuilder ClassChecker(Action<Type> checker)
|
||||
{
|
||||
if (checker == null)
|
||||
throw new ArgumentNullException(nameof(checker));
|
||||
|
||||
EventClassChecker = EventClassChecker == null
|
||||
? checker
|
||||
: EventClassChecker + checker;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ShrinkEventBusBuilder MarkerInterface<TMarker>() where TMarker : class
|
||||
{
|
||||
var markerType = typeof(TMarker);
|
||||
if (!markerType.IsInterface)
|
||||
throw new InvalidOperationException($"Marker type {markerType.FullName} must be an interface.");
|
||||
|
||||
return ClassChecker(eventType =>
|
||||
{
|
||||
if (!markerType.IsAssignableFrom(eventType))
|
||||
throw new ArgumentException(
|
||||
$"This bus only accepts events assignable to {markerType.FullName}, but got {eventType.FullName}.");
|
||||
});
|
||||
}
|
||||
|
||||
public IShrinkEventBus Build()
|
||||
{
|
||||
return new ShrinkEventBusInstance(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
public delegate UniTask ShrinkAsyncEventHandler<in TEvent>(TEvent eventData,
|
||||
CancellationToken cancellationToken) where TEvent : IShrinkEvent;
|
||||
|
||||
internal interface IShrinkEventChannel
|
||||
{
|
||||
int Count { get; }
|
||||
bool RemoveSubscription(long subscriptionId);
|
||||
void Clear();
|
||||
void DetachSlot(int slot);
|
||||
}
|
||||
|
||||
internal static class ShrinkEventChannelSlots<TEvent> where TEvent : IShrinkEvent
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
private static ShrinkEventChannel<TEvent>?[] _slots =
|
||||
Array.Empty<ShrinkEventChannel<TEvent>?>();
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ShrinkEventChannel<TEvent>? Get(int slot)
|
||||
{
|
||||
var snapshot = _slots;
|
||||
return (uint)slot < (uint)snapshot.Length ? snapshot[slot] : null;
|
||||
}
|
||||
|
||||
public static void Set(int slot, ShrinkEventChannel<TEvent> channel)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
var current = _slots;
|
||||
var length = current.Length;
|
||||
if (length <= slot)
|
||||
{
|
||||
length = Math.Max(4, length);
|
||||
while (length <= slot)
|
||||
length *= 2;
|
||||
}
|
||||
|
||||
var next = new ShrinkEventChannel<TEvent>?[length];
|
||||
Array.Copy(current, next, current.Length);
|
||||
next[slot] = channel;
|
||||
Volatile.Write(ref _slots, next);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Clear(int slot, ShrinkEventChannel<TEvent> channel)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
var current = _slots;
|
||||
if ((uint)slot >= (uint)current.Length || !ReferenceEquals(current[slot], channel))
|
||||
return;
|
||||
var next = (ShrinkEventChannel<TEvent>?[])current.Clone();
|
||||
next[slot] = null;
|
||||
Volatile.Write(ref _slots, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ShrinkEventChannel<TEvent> : IShrinkEventChannel
|
||||
where TEvent : IShrinkEvent
|
||||
{
|
||||
private static readonly bool SupportsCancellation =
|
||||
typeof(IShrinkCancelableEvent).IsAssignableFrom(typeof(TEvent));
|
||||
|
||||
private sealed class HandlerEntry
|
||||
{
|
||||
public long SubscriptionId;
|
||||
public long RegistrationOrder;
|
||||
public Action<TEvent>? SyncHandler;
|
||||
public ShrinkAsyncEventHandler<TEvent>? AsyncHandler;
|
||||
public ShrinkSubscribeDescriptor Descriptor;
|
||||
}
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly List<HandlerEntry> _entries = new();
|
||||
private HandlerEntry[] _snapshot = Array.Empty<HandlerEntry>();
|
||||
private Action<TEvent>? _syncDispatcher;
|
||||
|
||||
public int Count => _snapshot.Length;
|
||||
|
||||
public void Add(long subscriptionId, long registrationOrder, Action<TEvent> handler,
|
||||
ShrinkSubscribeDescriptor descriptor)
|
||||
{
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException(nameof(handler));
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_entries.Add(new HandlerEntry
|
||||
{
|
||||
SubscriptionId = subscriptionId,
|
||||
RegistrationOrder = registrationOrder,
|
||||
SyncHandler = handler,
|
||||
Descriptor = descriptor
|
||||
});
|
||||
RebuildSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(long subscriptionId, long registrationOrder,
|
||||
ShrinkAsyncEventHandler<TEvent> handler, ShrinkSubscribeDescriptor descriptor)
|
||||
{
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException(nameof(handler));
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_entries.Add(new HandlerEntry
|
||||
{
|
||||
SubscriptionId = subscriptionId,
|
||||
RegistrationOrder = registrationOrder,
|
||||
AsyncHandler = handler,
|
||||
Descriptor = descriptor
|
||||
});
|
||||
RebuildSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public ShrinkPostResult Post(in TEvent eventData)
|
||||
{
|
||||
var syncDispatcher = _syncDispatcher;
|
||||
if (syncDispatcher != null)
|
||||
{
|
||||
syncDispatcher(eventData);
|
||||
return new ShrinkPostResult(true, true, false);
|
||||
}
|
||||
|
||||
var snapshot = _snapshot;
|
||||
var handled = false;
|
||||
for (var i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
var entry = snapshot[i];
|
||||
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
|
||||
continue;
|
||||
|
||||
if (entry.SyncHandler != null)
|
||||
entry.SyncHandler(eventData);
|
||||
else if (entry.AsyncHandler != null)
|
||||
entry.AsyncHandler(eventData, CancellationToken.None).Forget(ShrinkEventDiagnostics.LogException);
|
||||
else
|
||||
continue;
|
||||
handled = true;
|
||||
}
|
||||
|
||||
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
|
||||
}
|
||||
|
||||
public async UniTask<ShrinkPostResult> PostAsync(TEvent eventData, ShrinkDispatchMode dispatchMode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var snapshot = _snapshot;
|
||||
var handled = false;
|
||||
if (dispatchMode == ShrinkDispatchMode.Parallel)
|
||||
{
|
||||
var tasks = new List<UniTask>(snapshot.Length);
|
||||
for (var i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var entry = snapshot[i];
|
||||
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
|
||||
continue;
|
||||
|
||||
if (entry.SyncHandler != null)
|
||||
entry.SyncHandler(eventData);
|
||||
else if (entry.AsyncHandler != null)
|
||||
tasks.Add(entry.AsyncHandler(eventData, cancellationToken));
|
||||
else
|
||||
continue;
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (tasks.Count > 0)
|
||||
await UniTask.WhenAll(tasks);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var entry = snapshot[i];
|
||||
if (IsCanceled(eventData) && !entry.Descriptor.ReceiveCanceled)
|
||||
continue;
|
||||
|
||||
if (entry.SyncHandler != null)
|
||||
entry.SyncHandler(eventData);
|
||||
else if (entry.AsyncHandler != null)
|
||||
await entry.AsyncHandler(eventData, cancellationToken);
|
||||
else
|
||||
continue;
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
return ShrinkPostResult.Completed(handled, IsCanceled(eventData));
|
||||
}
|
||||
|
||||
public bool RemoveSubscription(long subscriptionId)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
var removed = _entries.RemoveAll(entry => entry.SubscriptionId == subscriptionId) > 0;
|
||||
RebuildSnapshot();
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_entries.Clear();
|
||||
_snapshot = Array.Empty<HandlerEntry>();
|
||||
_syncDispatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void DetachSlot(int slot) => ShrinkEventChannelSlots<TEvent>.Clear(slot, this);
|
||||
|
||||
private void RebuildSnapshot()
|
||||
{
|
||||
_entries.Sort(static (left, right) =>
|
||||
{
|
||||
var leftOrder = left.Descriptor.NumericPriority == 0
|
||||
? (int)left.Descriptor.Priority * 1000
|
||||
: -left.Descriptor.NumericPriority;
|
||||
var rightOrder = right.Descriptor.NumericPriority == 0
|
||||
? (int)right.Descriptor.Priority * 1000
|
||||
: -right.Descriptor.NumericPriority;
|
||||
var priority = leftOrder.CompareTo(rightOrder);
|
||||
return priority != 0
|
||||
? priority
|
||||
: left.RegistrationOrder.CompareTo(right.RegistrationOrder);
|
||||
});
|
||||
_snapshot = _entries.ToArray();
|
||||
|
||||
Action<TEvent>? dispatcher = null;
|
||||
if (!SupportsCancellation)
|
||||
{
|
||||
for (var i = 0; i < _snapshot.Length; i++)
|
||||
{
|
||||
var handler = _snapshot[i].SyncHandler;
|
||||
if (handler == null)
|
||||
{
|
||||
dispatcher = null;
|
||||
break;
|
||||
}
|
||||
dispatcher += handler;
|
||||
}
|
||||
}
|
||||
Volatile.Write(ref _syncDispatcher, dispatcher);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsCanceled(TEvent eventData)
|
||||
{
|
||||
if (!SupportsCancellation)
|
||||
return false;
|
||||
return ((IShrinkCancelableEvent)(object)eventData!).IsCanceled;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc534f0e8598cab47b393ccffc90ad98
|
||||
guid: 5ffae3efb7664efcb82f2bbefbdd5a4e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,213 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
/// <summary>Marker shared by managed, Unity and ECS event payloads.</summary>
|
||||
public interface IShrinkEvent
|
||||
{
|
||||
}
|
||||
|
||||
public interface IShrinkCancelableEvent : IShrinkEvent
|
||||
{
|
||||
bool IsCanceled { get; }
|
||||
void SetCanceled(bool value);
|
||||
}
|
||||
|
||||
public interface IShrinkResultEvent<TResult> : IShrinkEvent
|
||||
{
|
||||
TResult Result { get; }
|
||||
void SetResult(TResult result);
|
||||
}
|
||||
|
||||
public enum ShrinkEventPriority
|
||||
{
|
||||
Highest = 0,
|
||||
High = 1,
|
||||
Normal = 2,
|
||||
Low = 3,
|
||||
Lowest = 4,
|
||||
Monitor = 5
|
||||
}
|
||||
|
||||
public enum ShrinkDispatchMode
|
||||
{
|
||||
Ordered = 0,
|
||||
Parallel = 1
|
||||
}
|
||||
|
||||
public enum ShrinkBusSchedulerKind
|
||||
{
|
||||
Inline = 0,
|
||||
MainThread = 1,
|
||||
DedicatedThread = 2,
|
||||
TaskPool = 3
|
||||
}
|
||||
|
||||
public enum ShrinkQueueOverflowPolicy
|
||||
{
|
||||
Reject = 0,
|
||||
DropNewest = 1,
|
||||
DropOldest = 2,
|
||||
Wait = 3
|
||||
}
|
||||
|
||||
public enum ShrinkPostFailure
|
||||
{
|
||||
None = 0,
|
||||
BusStopped = 1,
|
||||
QueueFull = 2,
|
||||
Canceled = 3,
|
||||
HandlerException = 4,
|
||||
InvalidEvent = 5
|
||||
}
|
||||
|
||||
public readonly struct ShrinkPostResult
|
||||
{
|
||||
private const int AcceptedMask = 1 << 0;
|
||||
private const int HandledMask = 1 << 1;
|
||||
private const int CanceledMask = 1 << 2;
|
||||
private const int FailureShift = 8;
|
||||
private readonly int _value;
|
||||
|
||||
public ShrinkPostResult(bool accepted, bool handled, bool canceled,
|
||||
ShrinkPostFailure failure = ShrinkPostFailure.None)
|
||||
{
|
||||
_value = (accepted ? AcceptedMask : 0) |
|
||||
(handled ? HandledMask : 0) |
|
||||
(canceled ? CanceledMask : 0) |
|
||||
((int)failure << FailureShift);
|
||||
}
|
||||
|
||||
public bool Accepted => (_value & AcceptedMask) != 0;
|
||||
public bool Handled => (_value & HandledMask) != 0;
|
||||
public bool Canceled => (_value & CanceledMask) != 0;
|
||||
public ShrinkPostFailure Failure => (ShrinkPostFailure)((uint)_value >> FailureShift);
|
||||
public bool Succeeded => Accepted && Failure == ShrinkPostFailure.None;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ShrinkPostResult Rejected(ShrinkPostFailure failure) =>
|
||||
new(false, false, false, failure);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static ShrinkPostResult Completed(bool handled, bool canceled = false) =>
|
||||
new(true, handled, canceled);
|
||||
}
|
||||
|
||||
public readonly struct ShrinkBusKey : IEquatable<ShrinkBusKey>
|
||||
{
|
||||
public ShrinkBusKey(string scope, string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scope))
|
||||
throw new ArgumentException("Bus scope must not be empty.", nameof(scope));
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
throw new ArgumentException("Bus name must not be empty.", nameof(name));
|
||||
|
||||
Scope = scope.Trim().ToLowerInvariant();
|
||||
Name = name.Trim();
|
||||
}
|
||||
|
||||
public string Scope { get; }
|
||||
public string Name { get; }
|
||||
public string Id => Scope == "game" && Name == "default" ? "game" : $"{Scope}:{Name}";
|
||||
|
||||
public static ShrinkBusKey Game => new("game", "default");
|
||||
public static ShrinkBusKey Server => new("server", "default");
|
||||
public static ShrinkBusKey Scene(string sceneId) => new("scene", sceneId);
|
||||
public static ShrinkBusKey Mod(string modId) => new("mod", modId);
|
||||
public static ShrinkBusKey World(string worldId) => new("world", worldId);
|
||||
|
||||
public static ShrinkBusKey Parse(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || string.Equals(value.Trim(), "game", StringComparison.OrdinalIgnoreCase))
|
||||
return Game;
|
||||
|
||||
var normalized = value.Trim();
|
||||
var separator = normalized.IndexOf(':');
|
||||
return separator <= 0 || separator == normalized.Length - 1
|
||||
? new ShrinkBusKey("custom", normalized)
|
||||
: new ShrinkBusKey(normalized.Substring(0, separator), normalized.Substring(separator + 1));
|
||||
}
|
||||
|
||||
public bool Equals(ShrinkBusKey other) =>
|
||||
string.Equals(Scope, other.Scope, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(Name, other.Name, StringComparison.Ordinal);
|
||||
|
||||
public override bool Equals(object? obj) => obj is ShrinkBusKey other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(Scope.ToLowerInvariant(), Name);
|
||||
public override string ToString() => Id;
|
||||
public static bool operator ==(ShrinkBusKey left, ShrinkBusKey right) => left.Equals(right);
|
||||
public static bool operator !=(ShrinkBusKey left, ShrinkBusKey right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
public sealed class ShrinkBusOptions
|
||||
{
|
||||
public ShrinkBusSchedulerKind Scheduler { get; set; } = ShrinkBusSchedulerKind.Inline;
|
||||
public ShrinkDispatchMode DispatchMode { get; set; } = ShrinkDispatchMode.Ordered;
|
||||
public int QueueCapacity { get; set; } = 4096;
|
||||
public int MaxConcurrency { get; set; } = Math.Max(1, Environment.ProcessorCount);
|
||||
public ShrinkQueueOverflowPolicy OverflowPolicy { get; set; } = ShrinkQueueOverflowPolicy.Reject;
|
||||
public bool DrainOnShutdown { get; set; } = true;
|
||||
public TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||
|
||||
public static ShrinkBusOptions Inline(ShrinkDispatchMode mode = ShrinkDispatchMode.Ordered) =>
|
||||
new() { Scheduler = ShrinkBusSchedulerKind.Inline, DispatchMode = mode };
|
||||
|
||||
public static ShrinkBusOptions MainThread(int queueCapacity = 4096) =>
|
||||
new() { Scheduler = ShrinkBusSchedulerKind.MainThread, QueueCapacity = queueCapacity };
|
||||
|
||||
public static ShrinkBusOptions DedicatedThread(int queueCapacity = 4096) =>
|
||||
new() { Scheduler = ShrinkBusSchedulerKind.DedicatedThread, QueueCapacity = queueCapacity };
|
||||
|
||||
public static ShrinkBusOptions TaskPool(int maxConcurrency = 0, int queueCapacity = 4096) =>
|
||||
new()
|
||||
{
|
||||
Scheduler = ShrinkBusSchedulerKind.TaskPool,
|
||||
DispatchMode = ShrinkDispatchMode.Parallel,
|
||||
QueueCapacity = queueCapacity,
|
||||
MaxConcurrency = maxConcurrency > 0 ? maxConcurrency : Math.Max(1, Environment.ProcessorCount)
|
||||
};
|
||||
|
||||
internal ShrinkBusOptions CloneValidated()
|
||||
{
|
||||
if (QueueCapacity <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(QueueCapacity));
|
||||
if (MaxConcurrency <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(MaxConcurrency));
|
||||
if (ShutdownTimeout < TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(ShutdownTimeout));
|
||||
if (OverflowPolicy == ShrinkQueueOverflowPolicy.Wait && Scheduler == ShrinkBusSchedulerKind.Inline)
|
||||
throw new InvalidOperationException("Inline buses do not have a queue and cannot use Wait overflow policy.");
|
||||
|
||||
return new ShrinkBusOptions
|
||||
{
|
||||
Scheduler = Scheduler,
|
||||
DispatchMode = DispatchMode,
|
||||
QueueCapacity = QueueCapacity,
|
||||
MaxConcurrency = MaxConcurrency,
|
||||
OverflowPolicy = OverflowPolicy,
|
||||
DrainOnShutdown = DrainOnShutdown,
|
||||
ShutdownTimeout = ShutdownTimeout
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ShrinkSubscribeDescriptor
|
||||
{
|
||||
public ShrinkSubscribeDescriptor(string? bus, ShrinkEventPriority priority,
|
||||
int numericPriority, bool receiveCanceled)
|
||||
{
|
||||
Bus = bus ?? string.Empty;
|
||||
Priority = priority;
|
||||
NumericPriority = numericPriority;
|
||||
ReceiveCanceled = receiveCanceled;
|
||||
}
|
||||
|
||||
public string Bus { get; }
|
||||
public ShrinkEventPriority Priority { get; }
|
||||
public int NumericPriority { get; }
|
||||
public bool ReceiveCanceled { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 04b409c00e3144fd8d9f3efb7c2901b7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,71 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkEventBus
|
||||
{
|
||||
/// <summary>
|
||||
/// Optional Unity lifecycle host for generated event bindings. It does not require
|
||||
/// the target component to inherit from an SDK base class.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public sealed class ShrinkMonoEventScope : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private string bus = "game";
|
||||
[SerializeField] private bool includeChildren;
|
||||
private IDisposable? _binding;
|
||||
private ShrinkEventBinding? _bindingGroup;
|
||||
|
||||
public string BusId
|
||||
{
|
||||
get => bus;
|
||||
set => bus = value ?? string.Empty;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
RefreshBindings();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
ReleaseBindings();
|
||||
}
|
||||
|
||||
public void RefreshBindings()
|
||||
{
|
||||
ReleaseBindings();
|
||||
var key = ShrinkBusKey.Parse(bus);
|
||||
if (!EventBus.TryGetBus(key, out var targetBus))
|
||||
targetBus = EventBus.GetOrCreateBus(key, ShrinkBusOptions.MainThread());
|
||||
|
||||
var components = includeChildren
|
||||
? GetComponentsInChildren<MonoBehaviour>(true)
|
||||
: GetComponents<MonoBehaviour>();
|
||||
for (var i = 0; i < components.Length; i++)
|
||||
TryAttach(components[i], targetBus);
|
||||
}
|
||||
|
||||
public void ReleaseBindings()
|
||||
{
|
||||
_binding?.Dispose();
|
||||
_binding = null;
|
||||
_bindingGroup = null;
|
||||
}
|
||||
|
||||
private void TryAttach(MonoBehaviour target, IShrinkEventBus targetBus)
|
||||
{
|
||||
if (target == null || ReferenceEquals(target, this))
|
||||
return;
|
||||
if (target is not IShrinkGeneratedSubscriber)
|
||||
return;
|
||||
|
||||
var binding = targetBus.Attach(target);
|
||||
if (_bindingGroup == null)
|
||||
_bindingGroup = new ShrinkEventBinding();
|
||||
_bindingGroup.Add(binding);
|
||||
_binding = _bindingGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fe48da79b2f4a10a338e497f4ce5ae1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ShrinkEventBus.PlayMode.Tests",
|
||||
"rootNamespace": "ShrinkEventBus.PlayMode.Tests",
|
||||
"references": [
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"optionalUnityReferences": ["TestAssemblies"],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": ["UNITY_INCLUDE_TESTS"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b03fa714413c4af5ad4140e7a409ef2f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Collections;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace ShrinkEventBus.PlayMode.Tests
|
||||
{
|
||||
internal readonly struct MonoLifecycleEvent : IShrinkEvent
|
||||
{
|
||||
public MonoLifecycleEvent(int value) => Value = value;
|
||||
public int Value { get; }
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||
internal sealed class MonoLifecycleTarget : MonoBehaviour
|
||||
{
|
||||
public int Sum { get; private set; }
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private void OnEvent(MonoLifecycleEvent value) => Sum += value.Value;
|
||||
}
|
||||
|
||||
public sealed class ShrinkMonoEventScopePlayModeTests
|
||||
{
|
||||
[UnityTest]
|
||||
public IEnumerator OnEnableAttachesAndOnDisableReleases()
|
||||
{
|
||||
var gameObject = new GameObject("ShrinkMonoEventScope-PlayMode");
|
||||
gameObject.SetActive(false);
|
||||
var target = gameObject.AddComponent<MonoLifecycleTarget>();
|
||||
gameObject.AddComponent<ShrinkMonoEventScope>();
|
||||
|
||||
gameObject.SetActive(true);
|
||||
yield return null;
|
||||
yield return EventBus.PostAsync(new MonoLifecycleEvent(3)).ToCoroutine();
|
||||
Assert.AreEqual(3, target.Sum);
|
||||
|
||||
gameObject.SetActive(false);
|
||||
yield return null;
|
||||
yield return EventBus.PostAsync(new MonoLifecycleEvent(5)).ToCoroutine();
|
||||
Assert.AreEqual(3, target.Sum);
|
||||
Object.Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f42b5f6f2249483b9978f0fa729d479b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,200 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ShrinkEventBus.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class EventBusDispatchTests
|
||||
{
|
||||
private class PlainEvent : EventBase
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class DerivedPlainEvent : PlainEvent
|
||||
{
|
||||
}
|
||||
|
||||
[Cancelable]
|
||||
private sealed class CancelableEvent : EventBase
|
||||
{
|
||||
}
|
||||
|
||||
[HasResult]
|
||||
private sealed class ResultEvent : EventBase
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TriggerEvent_ReturnsFalseWithoutSubscribers_TrueWhenHandled()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
Assert.IsFalse(bus.TriggerEvent(new PlainEvent()));
|
||||
|
||||
bus.RegisterEvent<PlainEvent>(_ => { }, EventPriority.NORMAL);
|
||||
Assert.IsTrue(bus.TriggerEvent(new PlainEvent()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanceledEvent_SkipsHandlersUnlessReceiveCanceled()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
bus.RegisterEvent<CancelableEvent>(evt =>
|
||||
{
|
||||
order.Add("canceler");
|
||||
evt.SetCanceled(true);
|
||||
}, EventPriority.HIGHEST);
|
||||
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
|
||||
bus.RegisterEvent<CancelableEvent>(evt => order.Add($"monitor:{evt.IsCanceled}"),
|
||||
EventPriority.MONITOR, receiveCanceled: true);
|
||||
|
||||
var evt = new CancelableEvent();
|
||||
bus.TriggerEvent(evt);
|
||||
|
||||
Assert.IsTrue(evt.IsCanceled);
|
||||
CollectionAssert.AreEqual(new[] { "canceler", "monitor:True" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetCanceled_OnNonCancelableEvent_ThrowsWithMessage()
|
||||
{
|
||||
var evt = new PlainEvent();
|
||||
|
||||
var ex = Assert.Throws<UnsupportedOperationException>(() => evt.SetCanceled(true));
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetResult_OnEventWithoutResult_ThrowsWithMessage()
|
||||
{
|
||||
var evt = new PlainEvent();
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => evt.SetResult(EventResult.ALLOW));
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(ex.Message));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SetResult_OnResultEvent_PersistsResult()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
bus.RegisterEvent<ResultEvent>(evt => evt.SetResult(EventResult.DENY), EventPriority.HIGH);
|
||||
|
||||
var evt = new ResultEvent();
|
||||
bus.TriggerEvent(evt);
|
||||
|
||||
Assert.AreEqual(EventResult.DENY, evt.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParentSubscriber_ReceivesDerivedEvent()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var hits = 0;
|
||||
bus.RegisterEvent<PlainEvent>(_ => hits++, EventPriority.NORMAL);
|
||||
|
||||
bus.TriggerEvent(new DerivedPlainEvent());
|
||||
|
||||
Assert.AreEqual(1, hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParentSubscriber_RegisteredAfterChildWasTriggered_StillReceivesChild()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
// 先触发子事件,让子类型的监听列表先于父监听器创建(覆盖脏传播路径)
|
||||
bus.TriggerEvent(new DerivedPlainEvent());
|
||||
|
||||
var hits = 0;
|
||||
bus.RegisterEvent<PlainEvent>(_ => hits++, EventPriority.NORMAL);
|
||||
bus.TriggerEvent(new DerivedPlainEvent());
|
||||
|
||||
Assert.AreEqual(1, hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerPhaseDispatch_OnlyRunsRequestedPhase()
|
||||
{
|
||||
var bus = EventBus.CreateBus(builder => builder.AllowPerPhaseDispatch());
|
||||
var order = new List<string>();
|
||||
|
||||
bus.RegisterEvent<PlainEvent>(_ => order.Add("high"), EventPriority.HIGH);
|
||||
bus.RegisterEvent<PlainEvent>(_ => order.Add("normal"), EventPriority.NORMAL);
|
||||
|
||||
bus.TriggerEvent(EventPriority.HIGH, new PlainEvent());
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "high" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PerPhaseDispatch_ThrowsWhenNotEnabled()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => bus.TriggerEvent(EventPriority.HIGH, new PlainEvent()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TriggerEventAsync_RunsHandlersSequentiallyByPriority()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
async UniTask AsyncHandler(PlainEvent evt)
|
||||
{
|
||||
order.Add("async-high");
|
||||
await UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<PlainEvent>(AsyncHandler, EventPriority.HIGH, receiveCanceled: false);
|
||||
bus.RegisterEvent<PlainEvent>(_ => order.Add("sync-normal"), EventPriority.NORMAL);
|
||||
|
||||
var handled = bus.TriggerEventAsync(new PlainEvent()).GetAwaiter().GetResult();
|
||||
|
||||
Assert.IsTrue(handled);
|
||||
CollectionAssert.AreEqual(new[] { "async-high", "sync-normal" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TriggerEventAsync_AsyncHandlerCancellation_SkipsLaterHandlers()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
async UniTask CancelingHandler(CancelableEvent evt)
|
||||
{
|
||||
evt.SetCanceled(true);
|
||||
order.Add("async-canceler");
|
||||
await UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
bus.RegisterEvent<CancelableEvent>(CancelingHandler, EventPriority.HIGHEST, receiveCanceled: false);
|
||||
bus.RegisterEvent<CancelableEvent>(_ => order.Add("skipped"), EventPriority.NORMAL);
|
||||
|
||||
bus.TriggerEventAsync(new CancelableEvent()).GetAwaiter().GetResult();
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "async-canceler" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetSubscribers_ReturnsDefensiveCopyOfDispatchSnapshot()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
bus.RegisterEvent<PlainEvent>(_ => { }, EventPriority.NORMAL);
|
||||
|
||||
var evt = new PlainEvent();
|
||||
bus.TriggerEvent(evt);
|
||||
|
||||
var first = evt.GetSubscribers();
|
||||
Assert.AreEqual(1, first.Length);
|
||||
|
||||
first[0] = null;
|
||||
var second = evt.GetSubscribers();
|
||||
Assert.IsNotNull(second[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace ShrinkEventBus.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class EventBusPriorityTests
|
||||
{
|
||||
private class BaseEvent : EventBase
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class DerivedEvent : BaseEvent
|
||||
{
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EnumPriority_ExecutesPhasesInOrder()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("LOW"), EventPriority.LOW);
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("MONITOR"), EventPriority.MONITOR);
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("HIGHEST"), EventPriority.HIGHEST);
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("NORMAL"), EventPriority.NORMAL);
|
||||
|
||||
bus.TriggerEvent(new BaseEvent());
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "HIGHEST", "NORMAL", "LOW", "MONITOR" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NumericPriority_HigherNumberRunsFirstWithinPhase()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
// 5 与 10 都映射到 NORMAL 档
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("p5"), 5);
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("p10"), 10);
|
||||
|
||||
bus.TriggerEvent(new BaseEvent());
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "p10", "p5" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NumericPriority_TieKeepsRegistrationOrder()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("first"), 0);
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("second"), 0);
|
||||
|
||||
bus.TriggerEvent(new BaseEvent());
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "first", "second" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NumericPriorityZero_MapsToNormalPhase()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
bus.RegisterEvent<BaseEvent>(_ => { }, 0);
|
||||
|
||||
var subscribers = bus.GetEventSubscribers<BaseEvent>();
|
||||
|
||||
Assert.AreEqual(1, subscribers.Length);
|
||||
Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterEvent_WithoutPriority_ResolvesToNormal()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
void Handler(BaseEvent evt)
|
||||
{
|
||||
}
|
||||
|
||||
// 不带优先级的调用应唯一解析到枚举重载(NORMAL),不再有重载二义性
|
||||
bus.RegisterEvent<BaseEvent>(Handler);
|
||||
|
||||
var subscribers = bus.GetEventSubscribers<BaseEvent>();
|
||||
Assert.AreEqual(1, subscribers.Length);
|
||||
Assert.AreEqual(EventPriority.NORMAL, subscribers[0].Priority);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParentAndChildHandlers_MergeByNumericPriorityWithinPhase()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent10"), 10);
|
||||
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child5"), 5);
|
||||
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child1"), 1);
|
||||
|
||||
bus.TriggerEvent(new DerivedEvent());
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "parent10", "child5", "child1" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ParentAndChildHandlers_TiePrefersChild()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var order = new List<string>();
|
||||
|
||||
bus.RegisterEvent<BaseEvent>(_ => order.Add("parent0"), 0);
|
||||
bus.RegisterEvent<DerivedEvent>(_ => order.Add("child0"), 0);
|
||||
|
||||
bus.TriggerEvent(new DerivedEvent());
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "child0", "parent0" }, order);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace ShrinkEventBus.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class EventBusRegistrationTests
|
||||
{
|
||||
private class ProbeEvent : EventBase
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class PooledEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
|
||||
protected override void OnReset()
|
||||
{
|
||||
Value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InstanceSubscriber
|
||||
{
|
||||
public int Hits;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnProbe(ProbeEvent evt)
|
||||
{
|
||||
Hits++;
|
||||
}
|
||||
}
|
||||
|
||||
private static class StaticSubscriber
|
||||
{
|
||||
public static int Hits;
|
||||
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private static void OnProbe(ProbeEvent evt)
|
||||
{
|
||||
Hits++;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InvalidSignatureSubscriber
|
||||
{
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnProbe(ProbeEvent evt, int extra)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[EventBusSubscriber]
|
||||
private sealed class EmptySubscriber
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class UnattributedSubscriber
|
||||
{
|
||||
[EventSubscribe(EventPriority.NORMAL)]
|
||||
private void OnProbe(ProbeEvent evt)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Register_InstanceScan_RegistersAndUnregisters()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var subscriber = new InstanceSubscriber();
|
||||
|
||||
bus.Register(subscriber);
|
||||
Assert.IsTrue(bus.IsInstanceRegistered(subscriber));
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
Assert.AreEqual(1, subscriber.Hits);
|
||||
|
||||
bus.Unregister(subscriber);
|
||||
Assert.IsFalse(bus.IsInstanceRegistered(subscriber));
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
Assert.AreEqual(1, subscriber.Hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Register_StaticTypeScan_RegistersAndUnregisters()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
StaticSubscriber.Hits = 0;
|
||||
|
||||
bus.Register(typeof(StaticSubscriber));
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
Assert.AreEqual(1, StaticSubscriber.Hits);
|
||||
|
||||
bus.Unregister(typeof(StaticSubscriber));
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
Assert.AreEqual(1, StaticSubscriber.Hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Register_MethodInfo_RegistersSingleHandler()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
StaticSubscriber.Hits = 0;
|
||||
var method = typeof(StaticSubscriber).GetMethod("OnProbe",
|
||||
BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Assert.IsNotNull(method);
|
||||
|
||||
bus.Register(method);
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
Assert.AreEqual(1, StaticSubscriber.Hits);
|
||||
|
||||
bus.Unregister(method);
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
Assert.AreEqual(1, StaticSubscriber.Hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Register_InvalidHandlerSignature_Throws()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => bus.Register(new InvalidSignatureSubscriber()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoRegister_TypeWithoutSubscriberAttribute_Throws()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => bus.AutoRegister(new UnattributedSubscriber()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AutoRegister_SubscriberWithoutHandlers_WarnsInsteadOfThrowing()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
|
||||
LogAssert.Expect(LogType.Warning, new Regex(@"has no \[EventSubscribe\] methods"));
|
||||
Assert.DoesNotThrow(() => bus.AutoRegister(new EmptySubscriber()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GetEventSubscribers_ReturnsDefensiveCopy()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
bus.RegisterEvent<ProbeEvent>(_ => { }, EventPriority.NORMAL);
|
||||
|
||||
var first = bus.GetEventSubscribers<ProbeEvent>();
|
||||
Assert.AreEqual(1, first.Length);
|
||||
|
||||
first[0] = null;
|
||||
var second = bus.GetEventSubscribers<ProbeEvent>();
|
||||
Assert.IsNotNull(second[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnregisterAllEventsForObject_RemovesScannedHandlers()
|
||||
{
|
||||
var bus = EventBus.CreateBus();
|
||||
var subscriber = new InstanceSubscriber();
|
||||
bus.Register(subscriber);
|
||||
|
||||
bus.UnregisterAllEventsForObject(subscriber);
|
||||
Assert.IsFalse(bus.IsInstanceRegistered(subscriber));
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
|
||||
Assert.AreEqual(0, subscriber.Hits);
|
||||
|
||||
bus.Register(subscriber);
|
||||
bus.TriggerEvent(new ProbeEvent());
|
||||
|
||||
Assert.AreEqual(1, subscriber.Hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EventPool_ReleaseResetsAndReusesInstance()
|
||||
{
|
||||
var evt = EventPool<PooledEvent>.Get();
|
||||
evt.Value = 42;
|
||||
|
||||
EventPool<PooledEvent>.Release(evt);
|
||||
var reused = EventPool<PooledEvent>.Get();
|
||||
|
||||
Assert.AreSame(evt, reused);
|
||||
Assert.AreEqual(0, reused.Value);
|
||||
|
||||
EventPool<PooledEvent>.Release(reused);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EventPool_DoubleRelease_DoesNotDuplicatePoolEntry()
|
||||
{
|
||||
var evt = EventPool<PooledEvent>.Get();
|
||||
|
||||
EventPool<PooledEvent>.Release(evt);
|
||||
EventPool<PooledEvent>.Release(evt);
|
||||
|
||||
var first = EventPool<PooledEvent>.Get();
|
||||
var second = EventPool<PooledEvent>.Get();
|
||||
|
||||
Assert.AreSame(evt, first);
|
||||
Assert.AreNotSame(evt, second);
|
||||
|
||||
EventPool<PooledEvent>.Release(first);
|
||||
EventPool<PooledEvent>.Release(second);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EventPool_DisposeReturnsToPool()
|
||||
{
|
||||
PooledEvent captured;
|
||||
using (var evt = EventPool<PooledEvent>.Get())
|
||||
{
|
||||
captured = evt;
|
||||
evt.Value = 7;
|
||||
}
|
||||
|
||||
var reused = EventPool<PooledEvent>.Get();
|
||||
Assert.AreSame(captured, reused);
|
||||
Assert.AreEqual(0, reused.Value);
|
||||
|
||||
EventPool<PooledEvent>.Release(reused);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4570cac2813143c40b9a8edeadf19057
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,94 +0,0 @@
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkEventBus.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public sealed class EventBusSubscriptionTests
|
||||
{
|
||||
private sealed class SampleEvent : EventBase
|
||||
{
|
||||
public int Value { get; set; }
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
EventBus.UnregisterAllEvents();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
EventBus.UnregisterAllEvents();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SubscribeEvent_Dispose_RemovesOnlyOwnedSubscription()
|
||||
{
|
||||
var firstHits = 0;
|
||||
var secondHits = 0;
|
||||
|
||||
using var first = EventBus.SubscribeEvent<SampleEvent>(_ => firstHits++);
|
||||
using var second = EventBus.SubscribeEvent<SampleEvent>(_ => secondHits++);
|
||||
|
||||
EventBus.TriggerEvent(new SampleEvent());
|
||||
Assert.AreEqual(1, firstHits);
|
||||
Assert.AreEqual(1, secondHits);
|
||||
|
||||
first.Dispose();
|
||||
|
||||
EventBus.TriggerEvent(new SampleEvent());
|
||||
Assert.AreEqual(1, firstHits);
|
||||
Assert.AreEqual(2, secondHits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ActiveSubscriptionSnapshot_ContainsExpectedMetadata()
|
||||
{
|
||||
using var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => { }, EventPriority.HIGH, true);
|
||||
|
||||
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
|
||||
|
||||
Assert.AreEqual(1, snapshots.Count);
|
||||
Assert.AreEqual(subscription.SubscriptionId, snapshots[0].SubscriptionId);
|
||||
Assert.AreEqual(typeof(SampleEvent), snapshots[0].EventType);
|
||||
Assert.AreEqual(EventPriority.HIGH, snapshots[0].Priority);
|
||||
Assert.IsTrue(snapshots[0].ReceiveCanceled);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(snapshots[0].MethodName));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RegisterEvent_StillWorksWithoutDisposableSubscription()
|
||||
{
|
||||
var hits = 0;
|
||||
|
||||
void Handler(SampleEvent evt)
|
||||
{
|
||||
hits += evt.Value;
|
||||
}
|
||||
|
||||
EventBus.RegisterEvent<SampleEvent>(Handler);
|
||||
EventBus.TriggerEvent(new SampleEvent { Value = 3 });
|
||||
|
||||
Assert.AreEqual(3, hits);
|
||||
|
||||
EventBus.UnregisterEvent<SampleEvent>(Handler);
|
||||
EventBus.TriggerEvent(new SampleEvent { Value = 5 });
|
||||
|
||||
Assert.AreEqual(3, hits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ActiveSubscriptionSnapshot_EmptyAfterDispose()
|
||||
{
|
||||
var subscription = EventBus.SubscribeEvent<SampleEvent>(_ => Debug.Log("noop"));
|
||||
subscription.Dispose();
|
||||
|
||||
var snapshots = EventBus.GetActiveSubscriptionsSnapshot();
|
||||
|
||||
Assert.AreEqual(0, snapshots.Count);
|
||||
Assert.IsTrue(subscription.IsDisposed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5ea531619ceaeb46af4f56454624ef7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,440 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
using UnityEngine.TestTools;
|
||||
|
||||
namespace ShrinkEventBus.Tests
|
||||
{
|
||||
internal readonly struct GeneratedPingEvent : IShrinkEvent
|
||||
{
|
||||
public GeneratedPingEvent(int value) => Value = value;
|
||||
public int Value { get; }
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||
internal sealed class CodeGeneratedTarget
|
||||
{
|
||||
public int Sum { get; private set; }
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||
internal static class StaticCodeGeneratedTarget
|
||||
{
|
||||
public static int Sum { get; set; }
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private static void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber(DefaultBus = "mod:late-static")]
|
||||
internal static class DelayedStaticCodeGeneratedTarget
|
||||
{
|
||||
public static int Sum { get; set; }
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private static void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||
}
|
||||
|
||||
[ShrinkEventSubscriber(DefaultBus = "game")]
|
||||
internal sealed class MonoCodeGeneratedTarget : MonoBehaviour
|
||||
{
|
||||
public int Sum { get; private set; }
|
||||
|
||||
[ShrinkSubscribe]
|
||||
private void OnPing(GeneratedPingEvent evt) => Sum += evt.Value;
|
||||
}
|
||||
|
||||
public sealed class ShrinkEventBusV2Tests
|
||||
{
|
||||
private readonly struct PingEvent : IShrinkEvent
|
||||
{
|
||||
public PingEvent(int value) => Value = value;
|
||||
public int Value { get; }
|
||||
}
|
||||
|
||||
private readonly struct DiagnosticEvent : IShrinkEvent
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class CancelEvent : IShrinkCancelableEvent
|
||||
{
|
||||
public bool IsCanceled { get; private set; }
|
||||
public void SetCanceled(bool value) => IsCanceled = value;
|
||||
}
|
||||
|
||||
private sealed class ResultEvent : IShrinkResultEvent<int>
|
||||
{
|
||||
public int Result { get; private set; }
|
||||
public void SetResult(int result) => Result = result;
|
||||
}
|
||||
|
||||
private sealed class GeneratedTarget : IShrinkGeneratedSubscriber
|
||||
{
|
||||
public int Sum;
|
||||
|
||||
public IDisposable AttachGenerated(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus = null)
|
||||
{
|
||||
var binding = new ShrinkEventBinding();
|
||||
binding.Add(ShrinkGeneratedBinding.Subscribe<PingEvent>(resolver, defaultBus, string.Empty,
|
||||
this, OnPing, ShrinkEventPriority.Normal, 0, false));
|
||||
return binding;
|
||||
}
|
||||
|
||||
private void OnPing(PingEvent evt) => Sum += evt.Value;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MultipleBusesAreIsolated()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
var mod = host.CreateBus(ShrinkBusKey.Mod("sample"), ShrinkBusOptions.Inline());
|
||||
var gameHits = 0;
|
||||
var modHits = 0;
|
||||
using var gameBinding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, evt => gameHits += evt.Value, ShrinkEventPriority.Normal, 0, false);
|
||||
using var modBinding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Mod("sample"),
|
||||
string.Empty, this, evt => modHits += evt.Value, ShrinkEventPriority.Normal, 0, false);
|
||||
|
||||
game.Post(new PingEvent(2));
|
||||
mod.Post(new PingEvent(3));
|
||||
|
||||
Assert.AreEqual(2, gameHits);
|
||||
Assert.AreEqual(3, modHits);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GeneratedTargetAttachDisposesAllHandlers()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
var target = new GeneratedTarget();
|
||||
var binding = host.Attach(target, ShrinkBusKey.Game);
|
||||
|
||||
game.Post(new PingEvent(4));
|
||||
binding.Dispose();
|
||||
game.Post(new PingEvent(7));
|
||||
|
||||
Assert.AreEqual(4, target.Sum);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AttributeOnlyTargetUsesIlGeneratedBinding()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
var target = new CodeGeneratedTarget();
|
||||
|
||||
using (host.Attach(target, ShrinkBusKey.Game))
|
||||
game.Post(new GeneratedPingEvent(9));
|
||||
game.Post(new GeneratedPingEvent(4));
|
||||
|
||||
Assert.AreEqual(9, target.Sum);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StaticAttributeSubscriberUsesModuleInitializerBinding()
|
||||
{
|
||||
StaticCodeGeneratedTarget.Sum = 0;
|
||||
|
||||
EventBus.Post(new GeneratedPingEvent(11));
|
||||
|
||||
Assert.AreEqual(11, StaticCodeGeneratedTarget.Sum);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void StaticSubscriberAttachesWhenNamedBusIsCreatedLater()
|
||||
{
|
||||
var key = ShrinkBusKey.Mod("late-static");
|
||||
EventBus.RemoveBus(key);
|
||||
DelayedStaticCodeGeneratedTarget.Sum = 0;
|
||||
var bus = EventBus.CreateBus(key, ShrinkBusOptions.Inline());
|
||||
|
||||
bus.Post(new GeneratedPingEvent(13));
|
||||
|
||||
Assert.AreEqual(13, DelayedStaticCodeGeneratedTarget.Sum);
|
||||
Assert.IsTrue(EventBus.RemoveBus(key));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator MonoScopeRefreshAndReleaseControlGeneratedBindings()
|
||||
{
|
||||
var gameObject = new GameObject("ShrinkEventBusV2-MonoScope");
|
||||
gameObject.SetActive(false);
|
||||
var target = gameObject.AddComponent<MonoCodeGeneratedTarget>();
|
||||
var scope = gameObject.AddComponent<ShrinkMonoEventScope>();
|
||||
Assert.IsInstanceOf<IShrinkGeneratedSubscriber>(target);
|
||||
|
||||
scope.RefreshBindings();
|
||||
yield return null;
|
||||
yield return EventBus.PostAsync(new GeneratedPingEvent(5)).ToCoroutine();
|
||||
Assert.AreEqual(5, target.Sum);
|
||||
|
||||
scope.ReleaseBindings();
|
||||
yield return EventBus.PostAsync(new GeneratedPingEvent(7)).ToCoroutine();
|
||||
Assert.AreEqual(5, target.Sum);
|
||||
UnityEngine.Object.DestroyImmediate(gameObject);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CanceledEventsSkipNormalHandlersButReachMonitors()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
var normal = 0;
|
||||
var monitor = 0;
|
||||
using var first = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, evt => evt.SetCanceled(true), ShrinkEventPriority.Highest, 0, false);
|
||||
using var second = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, _ => normal++, ShrinkEventPriority.Normal, 0, false);
|
||||
using var third = ShrinkGeneratedBinding.Subscribe<CancelEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, _ => monitor++, ShrinkEventPriority.Monitor, 0, true);
|
||||
|
||||
var result = game.Post(new CancelEvent());
|
||||
|
||||
Assert.IsTrue(result.Canceled);
|
||||
Assert.AreEqual(0, normal);
|
||||
Assert.AreEqual(1, monitor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PriorityAndNumericPriorityAreStable()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
var order = new List<string>();
|
||||
using var normal = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, _ => order.Add("normal"), ShrinkEventPriority.Normal, 0, false);
|
||||
using var high = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, _ => order.Add("high"), ShrinkEventPriority.High, 0, false);
|
||||
using var numeric = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, _ => order.Add("numeric"), ShrinkEventPriority.Normal, 75, false);
|
||||
|
||||
game.Post(new PingEvent(1));
|
||||
|
||||
CollectionAssert.AreEqual(new[] { "numeric", "high", "normal" }, order);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResultEventUsesTypedResultContract()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<ResultEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, value => value.SetResult(42),
|
||||
ShrinkEventPriority.Normal, 0, false);
|
||||
var value = new ResultEvent();
|
||||
|
||||
var result = game.Post(value);
|
||||
|
||||
Assert.IsTrue(result.Handled);
|
||||
Assert.AreEqual(42, value.Result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void HandlerExceptionPropagatesFromPost()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, _ => throw new InvalidOperationException("expected"),
|
||||
ShrinkEventPriority.Normal, 0, false);
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => game.Post(new PingEvent(1)));
|
||||
|
||||
Assert.AreEqual("expected", exception!.Message);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator PostAsyncCancellationReturnsCanceledFailure()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var bus = host.CreateBus(new ShrinkBusKey("worker", "cancellation"),
|
||||
ShrinkBusOptions.DedicatedThread());
|
||||
using var source = new CancellationTokenSource();
|
||||
source.Cancel();
|
||||
ShrinkPostResult result = default;
|
||||
|
||||
yield return bus.PostAsync(new PingEvent(1), source.Token)
|
||||
.ContinueWith(value => result = value)
|
||||
.ToCoroutine();
|
||||
|
||||
Assert.AreEqual(ShrinkPostFailure.Canceled, result.Failure);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DedicatedThreadShutdownDrainsAcceptedWork()
|
||||
{
|
||||
var host = new ShrinkEventBusHost();
|
||||
var key = new ShrinkBusKey("worker", "drain");
|
||||
var bus = host.CreateBus(key, ShrinkBusOptions.DedicatedThread(queueCapacity: 4));
|
||||
var hits = 0;
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, key,
|
||||
string.Empty, this, _ => Interlocked.Increment(ref hits),
|
||||
ShrinkEventPriority.Normal, 0, false);
|
||||
Assert.IsTrue(bus.Post(new PingEvent(1)).Accepted);
|
||||
Assert.IsTrue(bus.Post(new PingEvent(2)).Accepted);
|
||||
|
||||
yield return host.ShutdownAsync().ToCoroutine();
|
||||
|
||||
Assert.AreEqual(2, Volatile.Read(ref hits));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DedicatedThreadRejectsWhenBoundedQueueIsFull()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var key = new ShrinkBusKey("worker", "bounded");
|
||||
var options = ShrinkBusOptions.DedicatedThread(queueCapacity: 1);
|
||||
options.OverflowPolicy = ShrinkQueueOverflowPolicy.Reject;
|
||||
var bus = host.CreateBus(key, options);
|
||||
using var entered = new ManualResetEventSlim(false);
|
||||
using var release = new ManualResetEventSlim(false);
|
||||
var hits = 0;
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, key, string.Empty,
|
||||
this, _ =>
|
||||
{
|
||||
Interlocked.Increment(ref hits);
|
||||
entered.Set();
|
||||
release.Wait(TimeSpan.FromSeconds(2));
|
||||
}, ShrinkEventPriority.Normal, 0, false);
|
||||
|
||||
Assert.IsTrue(bus.Post(new PingEvent(1)).Accepted);
|
||||
Assert.IsTrue(entered.Wait(TimeSpan.FromSeconds(2)));
|
||||
Assert.IsTrue(bus.Post(new PingEvent(2)).Accepted);
|
||||
var rejected = bus.Post(new PingEvent(3));
|
||||
release.Set();
|
||||
|
||||
Assert.IsFalse(rejected.Accepted);
|
||||
Assert.AreEqual(ShrinkPostFailure.QueueFull, rejected.Failure);
|
||||
Assert.IsTrue(SpinWait.SpinUntil(() => Volatile.Read(ref hits) == 2, TimeSpan.FromSeconds(2)));
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator TaskPoolHonorsMaximumConcurrency()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var key = new ShrinkBusKey("worker", "parallel");
|
||||
var bus = host.CreateBus(key, ShrinkBusOptions.TaskPool(maxConcurrency: 2, queueCapacity: 8));
|
||||
var active = 0;
|
||||
var maxActive = 0;
|
||||
using var binding = ShrinkGeneratedBinding.SubscribeAsync<PingEvent>(host, key, string.Empty,
|
||||
this, async (_, _) =>
|
||||
{
|
||||
var current = Interlocked.Increment(ref active);
|
||||
UpdateMaximum(ref maxActive, current);
|
||||
Thread.Sleep(30);
|
||||
Interlocked.Decrement(ref active);
|
||||
await UniTask.CompletedTask;
|
||||
}, ShrinkEventPriority.Normal, 0, false);
|
||||
var posts = new List<UniTask<ShrinkPostResult>>();
|
||||
for (var i = 0; i < 4; i++)
|
||||
posts.Add(bus.PostAsync(new PingEvent(i)));
|
||||
|
||||
yield return UniTask.WhenAll(posts).ToCoroutine();
|
||||
|
||||
Assert.AreEqual(2, maxActive);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void WarmStructPostDoesNotAllocate()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var game = host.CreateBus(ShrinkBusKey.Game, ShrinkBusOptions.Inline());
|
||||
var sum = 0;
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host, ShrinkBusKey.Game,
|
||||
string.Empty, this, evt => sum += evt.Value, ShrinkEventPriority.Normal, 0, false);
|
||||
for (var i = 0; i < 100; i++)
|
||||
game.Post(new PingEvent(i));
|
||||
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (var i = 0; i < 10_000; i++)
|
||||
game.Post(new PingEvent(i));
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.AreEqual(0, allocated);
|
||||
Assert.Greater(sum, 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GlobalDetailedObserverReportsDispatchMetadata()
|
||||
{
|
||||
var captured = false;
|
||||
var trace = default(ShrinkEventTrace);
|
||||
void Capture(ShrinkEventTrace value)
|
||||
{
|
||||
trace = value;
|
||||
captured = true;
|
||||
}
|
||||
|
||||
EventBus.DetailedPosted += Capture;
|
||||
try
|
||||
{
|
||||
EventBus.Post(new DiagnosticEvent());
|
||||
}
|
||||
finally
|
||||
{
|
||||
EventBus.DetailedPosted -= Capture;
|
||||
}
|
||||
|
||||
Assert.IsTrue(captured);
|
||||
Assert.AreEqual(typeof(DiagnosticEvent), trace.EventType);
|
||||
Assert.AreEqual(ShrinkBusKey.Game, trace.BusKey);
|
||||
Assert.AreEqual(ShrinkBusSchedulerKind.MainThread, trace.Scheduler);
|
||||
Assert.IsTrue(trace.Result.Accepted);
|
||||
Assert.GreaterOrEqual(trace.ElapsedTimestampTicks, 0L);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GlobalStructPostDoesNotAllocateWithoutObservers()
|
||||
{
|
||||
var value = new DiagnosticEvent();
|
||||
for (var i = 0; i < 100; i++)
|
||||
EventBus.Post(in value);
|
||||
|
||||
var before = GC.GetAllocatedBytesForCurrentThread();
|
||||
for (var i = 0; i < 10_000; i++)
|
||||
EventBus.Post(in value);
|
||||
var allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
|
||||
Assert.AreEqual(0, allocated);
|
||||
}
|
||||
|
||||
[UnityTest]
|
||||
public IEnumerator DedicatedThreadPostAsyncRunsOnDedicatedThread()
|
||||
{
|
||||
using var host = new ShrinkEventBusHost();
|
||||
var bus = host.CreateBus(new ShrinkBusKey("worker", "tests"),
|
||||
ShrinkBusOptions.DedicatedThread());
|
||||
var callerThread = Thread.CurrentThread.ManagedThreadId;
|
||||
var handlerThread = callerThread;
|
||||
using var binding = ShrinkGeneratedBinding.Subscribe<PingEvent>(host,
|
||||
new ShrinkBusKey("worker", "tests"), string.Empty, this,
|
||||
_ => handlerThread = Thread.CurrentThread.ManagedThreadId,
|
||||
ShrinkEventPriority.Normal, 0, false);
|
||||
|
||||
yield return bus.PostAsync(new PingEvent(1)).ToCoroutine();
|
||||
|
||||
Assert.AreNotEqual(callerThread, handlerThread);
|
||||
}
|
||||
|
||||
private static void UpdateMaximum(ref int location, int candidate)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var current = Volatile.Read(ref location);
|
||||
if (candidate <= current || Interlocked.CompareExchange(ref location, candidate, current) == current)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f11f5ea66801460ca4ec08d35763d627
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "com.cneicy.shrink-eventbus",
|
||||
"version": "1.3.0",
|
||||
"version": "2.0.0",
|
||||
"displayName": "ShrinkEventBus",
|
||||
"description": "高性能、类型安全的 Unity 事件总线,支持优先级调度与自动注册。",
|
||||
"description": "多 Bus、特性强类型注册、UniTask 调度与低分配发布的 Unity 事件总线。",
|
||||
"unity": "2022.3",
|
||||
"documentationUrl": "https://github.com/cneicy/ShrinkEventBus",
|
||||
"changelogUrl": "https://github.com/cneicy/ShrinkEventBus/blob/main/CHANGELOG.md",
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
|
||||
本文件记录 `ShrinkModFramework` 在当前工作区中的包内变更。
|
||||
|
||||
## [Unreleased]
|
||||
## [0.2.0] - 2026-08-24
|
||||
|
||||
### Added
|
||||
|
||||
- 新增 `ShrinkModContextHost` / `ShrinkModComponentSource`,把模组生命周期、Registry、Network handler 与 Harmony lease 纳入可逆组件事务。
|
||||
- 新增真实外部 DLL fixture 测试,覆盖失败替换恢复、损坏文件、依赖缺失、watcher 去抖与删除卸载。
|
||||
- 注册表新增 owner namespace、显式优先级覆盖、同优先级冲突拒绝和覆盖候选诊断;owner 卸载时会恢复下一个覆盖或基础项。
|
||||
- `ShrinkModContext` 新增 `RegisterContent` / `OverrideContent`,模组不再手工传入 owner;查询改为 `IReadOnlyShrinkModRegistry<T>`。
|
||||
|
||||
### Changed
|
||||
|
||||
- `ShrinkModLoader` 默认使用 ContextHost;外部 DLL 以 SHA-256 作为 revision,同内容幂等。
|
||||
- watcher 监听新增、修改、删除与重命名,并把 burst 文件事件折叠为一次主线程组合提交。
|
||||
- 移除允许任意完整键的 `ShrinkModRegistry.Register(owner, key, value)`;基础内容必须通过 owner namespace 注册。
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
+4
-2
@@ -82,10 +82,11 @@ Application.persistentDataPath/Mods
|
||||
|
||||
- `[ShrinkMod]` 模组声明
|
||||
- `ShrinkModBase` 生命周期
|
||||
- `EventBusSubscriber` 实例自动接入
|
||||
- `[ShrinkEventSubscriber]` / `[ShrinkSubscribe]` 生成式实例接入 Mod Bus
|
||||
- `ShrinkCommandSubscriber` 实例命令自动接入
|
||||
- `ShrinkNetworkSubscriber` 实例网络处理自动接入
|
||||
- `context.GetRegistry(...)` 注册内容
|
||||
- `context.RegisterContent(...)` 使用 ModId namespace 注册内容
|
||||
- `context.OverrideContent(...)` 以显式优先级覆盖已有内容
|
||||
- `context.RegisterNetworkHandler(...)` 走模组网络抽象层
|
||||
|
||||
## 注意
|
||||
@@ -94,6 +95,7 @@ Application.persistentDataPath/Mods
|
||||
- 当前模板默认依赖当前仓库根目录下的:
|
||||
- `ShrinkModFramework.Runtime.csproj`
|
||||
- `ShrinkEventBus.Runtime.csproj`
|
||||
- `DotNet/ShrinkEventBus.Generator/ShrinkEventBus.Generator.csproj`(作为 Roslyn analyzer)
|
||||
- `ShrinkCommand.Runtime.csproj`
|
||||
- `ShrinkNetwork.Runtime.csproj`
|
||||
- 如果你把模板工程移到别处,需要同步修正 `__PROJECT_NAME__.csproj` 里的 `ProjectReference`
|
||||
|
||||
+3
@@ -16,6 +16,9 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkModFramework.Runtime.csproj" />
|
||||
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkEventBus.Runtime.csproj" />
|
||||
<ProjectReference Include="__SDK_ROOT_RELATIVE__\DotNet\ShrinkEventBus.Generator\ShrinkEventBus.Generator.csproj"
|
||||
OutputItemType="Analyzer"
|
||||
ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkCommand.Runtime.csproj" />
|
||||
<ProjectReference Include="__SDK_ROOT_RELATIVE__\ShrinkNetwork.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ using ShrinkNetwork;
|
||||
|
||||
namespace __ROOT_NAMESPACE__
|
||||
{
|
||||
public sealed class __PROJECT_NAME__Event : EventBase
|
||||
public sealed class __PROJECT_NAME__Event : IShrinkEvent
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
+4
-8
@@ -8,10 +8,10 @@ using ShrinkNetwork;
|
||||
namespace __ROOT_NAMESPACE__
|
||||
{
|
||||
[ShrinkMod("__MOD_ID__", "__DISPLAY_NAME__", "1.0.0")]
|
||||
[EventBusSubscriber]
|
||||
[ShrinkEventSubscriber(OwnerId = "__MOD_ID__", DefaultBus = "mod:__MOD_ID__")]
|
||||
[ShrinkCommandSubscriber]
|
||||
[ShrinkNetworkSubscriber]
|
||||
public sealed class __PROJECT_NAME__Mod : ShrinkModBase
|
||||
public sealed partial class __PROJECT_NAME__Mod : ShrinkModBase
|
||||
{
|
||||
private readonly RuntimeState _state = new();
|
||||
|
||||
@@ -22,11 +22,7 @@ namespace __ROOT_NAMESPACE__
|
||||
|
||||
public override void OnRegisterContent(ShrinkModContext context)
|
||||
{
|
||||
var registry = context.GetRegistry<string>("example.items");
|
||||
registry.Register(
|
||||
context.ModInfo.ModId,
|
||||
context.ModInfo.ModId + ":example_item",
|
||||
"__DISPLAY_NAME__ Item");
|
||||
context.RegisterContent("example.items", "example_item", "__DISPLAY_NAME__ Item");
|
||||
}
|
||||
|
||||
public override void OnInitialize(ShrinkModContext context)
|
||||
@@ -38,7 +34,7 @@ namespace __ROOT_NAMESPACE__
|
||||
});
|
||||
}
|
||||
|
||||
[EventSubscribe]
|
||||
[ShrinkSubscribe]
|
||||
private void OnExampleEvent(__PROJECT_NAME__Event evt)
|
||||
{
|
||||
_state.LastEventMessage = evt.Message ?? string.Empty;
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ using ShrinkNetwork;
|
||||
|
||||
namespace __ROOT_NAMESPACE__
|
||||
{
|
||||
public sealed class __PROJECT_NAME__Event : EventBase
|
||||
public sealed class __PROJECT_NAME__Event : IShrinkEvent
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
+4
-8
@@ -8,10 +8,10 @@ using ShrinkNetwork;
|
||||
namespace __ROOT_NAMESPACE__
|
||||
{
|
||||
[ShrinkMod("__MOD_ID__", "__DISPLAY_NAME__", "1.0.0")]
|
||||
[EventBusSubscriber]
|
||||
[ShrinkEventSubscriber(OwnerId = "__MOD_ID__", DefaultBus = "mod:__MOD_ID__")]
|
||||
[ShrinkCommandSubscriber]
|
||||
[ShrinkNetworkSubscriber]
|
||||
public sealed class __PROJECT_NAME__Mod : ShrinkModBase
|
||||
public sealed partial class __PROJECT_NAME__Mod : ShrinkModBase
|
||||
{
|
||||
private readonly RuntimeState _state = new();
|
||||
|
||||
@@ -22,11 +22,7 @@ namespace __ROOT_NAMESPACE__
|
||||
|
||||
public override void OnRegisterContent(ShrinkModContext context)
|
||||
{
|
||||
var registry = context.GetRegistry<string>("example.items");
|
||||
registry.Register(
|
||||
context.ModInfo.ModId,
|
||||
context.ModInfo.ModId + ":example_item",
|
||||
"__DISPLAY_NAME__ Item");
|
||||
context.RegisterContent("example.items", "example_item", "__DISPLAY_NAME__ Item");
|
||||
}
|
||||
|
||||
public override void OnInitialize(ShrinkModContext context)
|
||||
@@ -38,7 +34,7 @@ namespace __ROOT_NAMESPACE__
|
||||
});
|
||||
}
|
||||
|
||||
[EventSubscribe]
|
||||
[ShrinkSubscribe]
|
||||
private void OnExampleEvent(__PROJECT_NAME__Event evt)
|
||||
{
|
||||
_state.LastEventMessage = evt.Message ?? string.Empty;
|
||||
|
||||
@@ -20,6 +20,7 @@ public static class ShrinkModSdkExporter
|
||||
{
|
||||
"ShrinkModFramework.Runtime",
|
||||
"ShrinkEventBus.Runtime",
|
||||
"ShrinkEventBus.Generator",
|
||||
"ShrinkCommand.Runtime",
|
||||
"ShrinkCommand.Integration.EventBus",
|
||||
"ShrinkCommand.Integration.Network",
|
||||
@@ -43,6 +44,7 @@ public static class ShrinkModSdkExporter
|
||||
{
|
||||
var projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
||||
BuildSolution(projectRoot);
|
||||
BuildEventBusGenerator(projectRoot);
|
||||
|
||||
var defaultOutputRoot = Path.Combine(projectRoot, DefaultOutputRootRelativePath);
|
||||
Directory.CreateDirectory(defaultOutputRoot);
|
||||
@@ -109,6 +111,30 @@ public static class ShrinkModSdkExporter
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildEventBusGenerator(string projectRoot)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
Arguments = "build .\\DotNet\\ShrinkEventBus.Generator\\ShrinkEventBus.Generator.csproj -c Release /nr:false",
|
||||
WorkingDirectory = projectRoot,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
throw new InvalidOperationException("无法启动 ShrinkEventBus.Generator 构建。");
|
||||
|
||||
var stdout = process.StandardOutput.ReadToEnd();
|
||||
var stderr = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
if (process.ExitCode != 0)
|
||||
throw new InvalidOperationException("构建 ShrinkEventBus.Generator 失败。\n" + stdout + "\n" + stderr);
|
||||
}
|
||||
|
||||
private static void PrepareOutputDirectory(string outputRoot)
|
||||
{
|
||||
Directory.CreateDirectory(outputRoot);
|
||||
@@ -135,6 +161,7 @@ public static class ShrinkModSdkExporter
|
||||
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "Assembly-CSharp"),
|
||||
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkModFramework.Runtime"),
|
||||
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkEventBus.Runtime"),
|
||||
Path.Combine(projectRoot, "DotNet", "ShrinkEventBus.Generator", "bin", "Release", "netstandard2.0"),
|
||||
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkCommand.Runtime"),
|
||||
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkCommand.Integration.EventBus"),
|
||||
Path.Combine(projectRoot, "Temp", "Bin", "Debug", "ShrinkCommand.Integration.Network"),
|
||||
@@ -181,7 +208,7 @@ public static class ShrinkModSdkExporter
|
||||
builder.AppendLine("## 内容");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("- `Libs/`");
|
||||
builder.AppendLine(" - 已编译好的框架运行时 DLL、当前游戏 DLL,以及模板需要的基础依赖");
|
||||
builder.AppendLine(" - 已编译好的框架运行时 DLL、EventBus 代码生成器、当前游戏 DLL,以及模板需要的基础依赖");
|
||||
builder.AppendLine("- `Templates/ExternalMod/SampleShrinkMod/`");
|
||||
builder.AppendLine(" - 可直接复制一份开始写外部模组的模板工程");
|
||||
builder.AppendLine();
|
||||
@@ -266,6 +293,8 @@ public static class ShrinkModSdkExporter
|
||||
- `src/`
|
||||
- 示例模组代码
|
||||
|
||||
EventBus handler 只使用 `[ShrinkEventSubscriber]` / `[ShrinkSubscribe]`。项目已把 `Libs/ShrinkEventBus.Generator.dll` 作为 Roslyn analyzer 引入,因此 Attach/Post 路径不依赖运行时反射。
|
||||
|
||||
## 推荐构建方式
|
||||
|
||||
双击或在命令行运行:
|
||||
@@ -367,6 +396,10 @@ cmd /k";
|
||||
builder.AppendLine(" </Reference>");
|
||||
}
|
||||
|
||||
builder.AppendLine(" </ItemGroup>");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" <ItemGroup>");
|
||||
builder.AppendLine(" <Analyzer Include=\"..\\..\\..\\Libs\\ShrinkEventBus.Generator.dll\" />");
|
||||
builder.AppendLine(" </ItemGroup>");
|
||||
builder.AppendLine("</Project>");
|
||||
return builder.ToString();
|
||||
@@ -381,7 +414,7 @@ using ShrinkNetwork;
|
||||
|
||||
namespace SampleShrinkMod
|
||||
{
|
||||
public sealed class SampleShrinkModEvent : EventBase
|
||||
public sealed class SampleShrinkModEvent : IShrinkEvent
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -413,10 +446,10 @@ using ShrinkNetwork;
|
||||
namespace SampleShrinkMod
|
||||
{
|
||||
[ShrinkMod(""sample.shrink.mod"", ""Sample Shrink Mod"", ""1.0.0"")]
|
||||
[EventBusSubscriber]
|
||||
[ShrinkEventSubscriber(OwnerId = ""sample.shrink.mod"", DefaultBus = ""mod:sample.shrink.mod"")]
|
||||
[ShrinkCommandSubscriber]
|
||||
[ShrinkNetworkSubscriber]
|
||||
public sealed class SampleShrinkModMod : ShrinkModBase
|
||||
public sealed partial class SampleShrinkModMod : ShrinkModBase
|
||||
{
|
||||
private readonly RuntimeState _state = new();
|
||||
|
||||
@@ -427,11 +460,7 @@ namespace SampleShrinkMod
|
||||
|
||||
public override void OnRegisterContent(ShrinkModContext context)
|
||||
{
|
||||
var registry = context.GetRegistry<string>(""example.items"");
|
||||
registry.Register(
|
||||
context.ModInfo.ModId,
|
||||
context.ModInfo.ModId + "":example_item"",
|
||||
""Sample Shrink Mod Item"");
|
||||
context.RegisterContent(""example.items"", ""example_item"", ""Sample Shrink Mod Item"");
|
||||
}
|
||||
|
||||
public override void OnInitialize(ShrinkModContext context)
|
||||
@@ -443,7 +472,7 @@ namespace SampleShrinkMod
|
||||
});
|
||||
}
|
||||
|
||||
[EventSubscribe]
|
||||
[ShrinkSubscribe]
|
||||
private void OnExampleEvent(SampleShrinkModEvent evt)
|
||||
{
|
||||
_state.LastEventMessage = evt.Message ?? string.Empty;
|
||||
|
||||
@@ -125,8 +125,7 @@ public class DemoCoreMod : ShrinkModBase
|
||||
{
|
||||
public override void OnRegisterContent(ShrinkModContext context)
|
||||
{
|
||||
var itemRegistry = context.GetRegistry<string>("items");
|
||||
itemRegistry.Register(context.ModInfo.ModId, "demo:iron_hammer", "Iron Hammer");
|
||||
context.RegisterContent("items", "iron_hammer", "Iron Hammer");
|
||||
}
|
||||
|
||||
public override void OnInitialize(ShrinkModContext context)
|
||||
@@ -250,14 +249,27 @@ var snapshot = ShrinkModDiagnostics.CaptureExternalAssemblies(settings);
|
||||
### 注册内容
|
||||
|
||||
```csharp
|
||||
var itemRegistry = context.GetRegistry<string>("items");
|
||||
itemRegistry.Register(context.ModInfo.ModId, "demo:sword", "Sword");
|
||||
var key = context.RegisterContent("items", "sword", "Sword");
|
||||
// key == "demo.core:sword"
|
||||
```
|
||||
|
||||
基础注册不接受任意完整键。框架始终使用当前 `ModId` 生成 `<owner>:<localKey>`,避免模组误写其它 owner 的 namespace。
|
||||
|
||||
### 覆盖已有内容
|
||||
|
||||
```csharp
|
||||
context.OverrideContent(
|
||||
"items",
|
||||
"core:iron_sword",
|
||||
priority: 100,
|
||||
value: "Overridden Sword");
|
||||
```
|
||||
|
||||
### 读取内容
|
||||
|
||||
```csharp
|
||||
if (itemRegistry.TryGet("demo:sword", out var itemName))
|
||||
var itemRegistry = context.GetRegistry<string>("items");
|
||||
if (itemRegistry.TryGet("demo.core:sword", out var itemName))
|
||||
{
|
||||
context.Log($"找到内容:{itemName}");
|
||||
}
|
||||
@@ -265,8 +277,12 @@ if (itemRegistry.TryGet("demo:sword", out var itemName))
|
||||
|
||||
规则:
|
||||
|
||||
- 一个注册表内的键必须唯一
|
||||
- 每条注册项都记录归属模组
|
||||
- 基础项的完整键固定为 `<ownerModId>:<localKey>`
|
||||
- `context.GetRegistry<T>()` 只返回 `IReadOnlyShrinkModRegistry<T>`;写入只能经过 `RegisterContent` / `OverrideContent`
|
||||
- 覆盖目标必须已经存在
|
||||
- 优先级越高越先命中;同目标、同优先级直接报冲突
|
||||
- 每条基础项和覆盖项都记录 owner;owner 卸载时自动撤回,并恢复下一个覆盖或基础值
|
||||
- `Entries` 返回当前生效项,`BaseEntries` 返回未应用覆盖的基础项
|
||||
- 注册表按类型和名称双重隔离
|
||||
|
||||
## 网络同步
|
||||
@@ -345,7 +361,7 @@ ShrinkModNetworkManager.SetTransport(new MyTransport());
|
||||
|
||||
之后,框架会自动尝试把**模组实例本身**接入这些现有模块:
|
||||
|
||||
- 如果模组类带 `[EventBusSubscriber]`,自动调用 `EventBus.AutoRegister(modInstance)`
|
||||
- 如果模组类带 `[ShrinkEventSubscriber]` 且携带生成绑定,自动 `Attach` 到 `ShrinkBusKey.Mod(modId)`,并在模组生命周期结束时释放 binding
|
||||
- 如果模组类带 `[ShrinkCommandSubscriber]`,自动调用 `ShrinkCommandRuntime.Default.RegisterCommands(modInstance)`
|
||||
- 如果模组类带 `[ShrinkNetworkSubscriber]`,自动调用 `ShrinkNetworkRuntime.Default.RegisterHandlers(modInstance)`
|
||||
- 如果项目里存在 `ShrinkNetwork.Integration.EventBus`,会额外调用 `ShrinkNetworkEventBusBridge.RefreshBindings()`,把新模组里声明的网络事件类型补进桥接层
|
||||
@@ -354,7 +370,7 @@ ShrinkModNetworkManager.SetTransport(new MyTransport());
|
||||
|
||||
- **项目内模组**和**运行时外部 DLL 模组**都可以把实例方法挂到 `EventBus / Command / Network`
|
||||
- 模组作者不需要自己再写一遍宿主层胶水
|
||||
- `ShrinkModFramework` 本身仍然没有硬引用这些包,保持可选依赖关系
|
||||
- EventBus 是模组框架的正式依赖;Command、Network 及其桥接仍按安装情况接入
|
||||
|
||||
### 推荐写法
|
||||
|
||||
@@ -369,10 +385,10 @@ using ShrinkModFramework;
|
||||
using ShrinkNetwork;
|
||||
|
||||
[ShrinkMod("demo.full", "Demo Full", "1.0.0")]
|
||||
[EventBusSubscriber]
|
||||
[ShrinkEventSubscriber(OwnerId = "demo.full", DefaultBus = "mod:demo.full")]
|
||||
[ShrinkCommandSubscriber]
|
||||
[ShrinkNetworkSubscriber]
|
||||
public sealed class DemoFullMod : ShrinkModBase
|
||||
public sealed partial class DemoFullMod : ShrinkModBase
|
||||
{
|
||||
private DemoSaveData _saveData = new();
|
||||
|
||||
@@ -390,7 +406,7 @@ public sealed class DemoFullMod : ShrinkModBase
|
||||
});
|
||||
}
|
||||
|
||||
[EventSubscribe]
|
||||
[ShrinkSubscribe]
|
||||
private void OnSaveCompleted(ShrinkDataSaver.Integration.SaveCompletedEvent evt)
|
||||
{
|
||||
// 这里只是示意:模组类实例会被框架自动接到 EventBus
|
||||
@@ -426,15 +442,16 @@ public sealed class DemoFullMod : ShrinkModBase
|
||||
|
||||
推荐:
|
||||
|
||||
- 模组类本身带 `[EventBusSubscriber]`
|
||||
- 实例方法上写 `[EventSubscribe]`
|
||||
- 由 `ShrinkModFramework` 自动 `AutoRegister`
|
||||
- 模组类本身带 `[ShrinkEventSubscriber]` 并声明 `DefaultBus = "mod:<modId>"`
|
||||
- 实例方法上写 `[ShrinkSubscribe]`
|
||||
- 类型声明为 `partial`,由 ILPostProcessor 或 Roslyn incremental generator 生成强类型 binding
|
||||
- 由 `ShrinkModFramework` 自动 Attach 到模组 Bus,并随模组生命周期释放
|
||||
|
||||
注意:
|
||||
|
||||
- `ShrinkEventBus` 的 `MonoBehaviour` 零侵入自动注册依赖编译期织入
|
||||
- 对**运行时外部 DLL**,不要假设它也会自动织入生命周期
|
||||
- 外部 DLL 更稳的方式是“实例订阅 + 框架自动 `EventBus.AutoRegister(modInstance)`”
|
||||
- Unity 项目内类型由 ILPostProcessor 生成 binding;外部 DLL 使用导出 SDK 中的 `ShrinkEventBus.Generator` 分析器
|
||||
- 外部 DLL 必须携带生成合同;正式运行时不会反射扫描没有生成合同的旧程序集
|
||||
- 模组框架只负责把已生成的实例 binding Attach 到对应 Mod Bus,不会枚举方法或调用 `MethodInfo.Invoke`
|
||||
|
||||
#### 2. `ShrinkDataSaver`
|
||||
|
||||
@@ -503,7 +520,7 @@ public sealed class DemoFullMod : ShrinkModBase
|
||||
|
||||
推荐:
|
||||
|
||||
- 事件类型同时满足 `EventBase + IShrinkNetworkMessage`
|
||||
- 事件类型同时满足 `IShrinkEvent + IShrinkNetworkMessage`
|
||||
- 标记 `[ShrinkNetworkEvent] + [ShrinkNetworkMessage(...)]`
|
||||
- 让模组监听或发布这些事件,而不是再写一层重复 DTO
|
||||
|
||||
@@ -522,10 +539,10 @@ public sealed class DemoFullMod : ShrinkModBase
|
||||
|
||||
- **项目内模组**
|
||||
- 编译时就在 Unity 当前 AppDomain 里
|
||||
- 更容易吃到各模块自己的静态扫描/自动注册能力
|
||||
- 由 Unity ILPostProcessor 生成 EventBus binding
|
||||
- **运行时外部 DLL 模组**
|
||||
- 是 `ShrinkModFramework` 后续动态加载进来的
|
||||
- 更稳的是走“模组实例注册”而不是“静态全局扫描”
|
||||
- 必须引用导出包里的 `ShrinkEventBus.Generator.dll` 生成同一份 binding 合同
|
||||
|
||||
所以当前推荐原则很明确:
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using ShrinkContext;
|
||||
using ShrinkEventBus;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
@@ -66,6 +67,16 @@ namespace ShrinkModFramework
|
||||
instance.OnRegisterContent(modContext);
|
||||
handle.State = ShrinkModState.ContentRegistered;
|
||||
|
||||
IDisposable eventBinding = null;
|
||||
ctx.Effect(
|
||||
() => eventBinding = ShrinkModOptionalRuntimeIntegration.TryAttachEventBusInstance(
|
||||
instance.GetType(), instance, handle.Info.ModId, _host.VerboseLogging),
|
||||
() =>
|
||||
{
|
||||
eventBinding?.Dispose();
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
});
|
||||
|
||||
// 模组网络 handler 以 ModId 为归属键,可在失败或卸载时整体撤回。
|
||||
ctx.Effect(() => { }, () => ShrinkModNetworkManager.UnregisterHandlers(handle.Info.ModId));
|
||||
instance.OnInitialize(modContext);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace ShrinkModFramework
|
||||
public IReadOnlyList<ShrinkModComponentSource> CurrentSources => _currentSources.ToArray();
|
||||
internal ShrinkModRegistryManager RegistryManager { get; }
|
||||
|
||||
public ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
public IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public static string GetModKey(string modId)
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace ShrinkModFramework
|
||||
return _host.Mods;
|
||||
}
|
||||
|
||||
public static ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
public static IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
Host.GetOrCreateRegistry<T>(name);
|
||||
|
||||
internal static void ResetForDomainReload()
|
||||
|
||||
@@ -27,11 +27,22 @@ namespace ShrinkModFramework
|
||||
public IReadOnlyDictionary<string, ShrinkModHandle> LoadedMods => _loadedMods;
|
||||
public bool IsContextManaged => _effectContext != null;
|
||||
|
||||
public ShrinkModRegistry<T> GetRegistry<T>(string name) => _registryManager.GetOrCreateRegistry<T>(name);
|
||||
public IReadOnlyShrinkModRegistry<T> GetRegistry<T>(string name) =>
|
||||
_registryManager.GetOrCreateRegistry<T>(name);
|
||||
|
||||
public bool TryGetRegistry<T>(string name, out ShrinkModRegistry<T> registry)
|
||||
public bool TryGetRegistry<T>(string name, out IReadOnlyShrinkModRegistry<T> registry)
|
||||
=> _registryManager.TryGetRegistry(name, out registry);
|
||||
|
||||
/// <summary>用当前 ModId 作为 namespace 注册内容,并返回最终完整键。</summary>
|
||||
public string RegisterContent<T>(string registryName, string localKey, T value) =>
|
||||
_registryManager.GetOrCreateMutableRegistry<T>(registryName)
|
||||
.RegisterNamespaced(ModInfo.ModId, localKey, value);
|
||||
|
||||
/// <summary>以当前模组为 owner 覆盖已有内容;同目标、同优先级冲突会直接失败。</summary>
|
||||
public void OverrideContent<T>(string registryName, string targetKey, int priority, T value) =>
|
||||
_registryManager.GetOrCreateMutableRegistry<T>(registryName)
|
||||
.RegisterOverride(ModInfo.ModId, targetKey, priority, value);
|
||||
|
||||
public bool IsModLoaded(string modId) => !string.IsNullOrWhiteSpace(modId) && _loadedMods.ContainsKey(modId);
|
||||
|
||||
public bool TryGetLoadedMod(string modId, out ShrinkModHandle handle)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public sealed class ShrinkModHandle
|
||||
@@ -6,6 +8,7 @@ namespace ShrinkModFramework
|
||||
public IShrinkMod Instance { get; }
|
||||
public ShrinkModState State { get; internal set; }
|
||||
public long Generation { get; internal set; }
|
||||
internal IDisposable EventBinding { get; set; }
|
||||
|
||||
internal ShrinkModHandle(ShrinkModInfo info, IShrinkMod instance)
|
||||
{
|
||||
|
||||
+15
-16
@@ -1,21 +1,24 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
internal static class ShrinkModOptionalRuntimeIntegration
|
||||
{
|
||||
public static void RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo, bool verboseLogging)
|
||||
public static IDisposable RegisterModInstance(IShrinkMod modInstance, ShrinkModInfo modInfo,
|
||||
bool verboseLogging)
|
||||
{
|
||||
if (modInstance == null || modInfo == null)
|
||||
return;
|
||||
return null;
|
||||
|
||||
var modType = modInstance.GetType();
|
||||
TryRegisterEventBusInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
var eventBinding = TryAttachEventBusInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
TryRegisterCommandInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
TryRegisterNetworkInstance(modType, modInstance, modInfo.ModId, verboseLogging);
|
||||
return eventBinding;
|
||||
}
|
||||
|
||||
public static void RefreshGlobalBindings(bool verboseLogging)
|
||||
@@ -23,29 +26,25 @@ namespace ShrinkModFramework
|
||||
TryRefreshNetworkEventBusBridge(verboseLogging);
|
||||
}
|
||||
|
||||
private static void TryRegisterEventBusInstance(Type modType, object modInstance, string modId, bool verboseLogging)
|
||||
public static IDisposable TryAttachEventBusInstance(Type modType, object modInstance,
|
||||
string modId, bool verboseLogging)
|
||||
{
|
||||
if (!HasAttribute(modType, "ShrinkEventBus.EventBusSubscriberAttribute", "ShrinkEventBus.Runtime"))
|
||||
return;
|
||||
if (modInstance is not IShrinkGeneratedSubscriber)
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var eventBusType = FindType("ShrinkEventBus.EventBus", "ShrinkEventBus.Runtime");
|
||||
var autoRegisterMethod = eventBusType?.GetMethod("AutoRegister",
|
||||
BindingFlags.Public | BindingFlags.Static,
|
||||
null,
|
||||
new[] { typeof(object) },
|
||||
null);
|
||||
if (autoRegisterMethod == null)
|
||||
return;
|
||||
|
||||
autoRegisterMethod.Invoke(null, new[] { modInstance });
|
||||
var busKey = ShrinkBusKey.Mod(modId);
|
||||
EventBus.GetOrCreateBus(busKey, ShrinkBusOptions.DedicatedThread());
|
||||
var binding = EventBus.Attach(modInstance, busKey);
|
||||
if (verboseLogging)
|
||||
Debug.Log($"[ShrinkModFramework] 模组 {modId} 已接入 ShrinkEventBus 实例订阅。");
|
||||
return binding;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogWarning($"[ShrinkModFramework] 模组 {modId} 接入 ShrinkEventBus 失败:{ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using ShrinkEventBus;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
@@ -106,6 +107,9 @@ namespace ShrinkModFramework
|
||||
{
|
||||
foreach (var handle in newHandles)
|
||||
{
|
||||
handle.EventBinding?.Dispose();
|
||||
handle.EventBinding = null;
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
LoadedMods.Remove(handle.Info.ModId);
|
||||
LoadSequence.Remove(handle);
|
||||
}
|
||||
@@ -123,7 +127,7 @@ namespace ShrinkModFramework
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
public static IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
=> ShrinkModCordisRuntime.IsInitialized
|
||||
? ShrinkModCordisRuntime.GetOrCreateRegistry<T>(name)
|
||||
: RegistryManager.GetOrCreateRegistry<T>(name);
|
||||
@@ -131,6 +135,7 @@ namespace ShrinkModFramework
|
||||
internal static void ResetForTesting()
|
||||
{
|
||||
ShrinkModCordisRuntime.ResetForTesting();
|
||||
DisposeLegacyEventBindings();
|
||||
LoadedMods.Clear();
|
||||
LoadSequence.Clear();
|
||||
RegistryManager.Clear();
|
||||
@@ -145,6 +150,7 @@ namespace ShrinkModFramework
|
||||
internal static void ResetForDomainReload()
|
||||
{
|
||||
ShrinkModCordisRuntime.ResetForDomainReload();
|
||||
DisposeLegacyEventBindings();
|
||||
LoadedMods.Clear();
|
||||
LoadSequence.Clear();
|
||||
RegistryManager.Clear();
|
||||
@@ -383,11 +389,22 @@ namespace ShrinkModFramework
|
||||
private static void RegisterOptionalRuntimeIntegrations(IEnumerable<ShrinkModHandle> handles, bool verboseLogging)
|
||||
{
|
||||
foreach (var handle in handles)
|
||||
ShrinkModOptionalRuntimeIntegration.RegisterModInstance(handle.Instance, handle.Info, verboseLogging);
|
||||
handle.EventBinding = ShrinkModOptionalRuntimeIntegration.RegisterModInstance(
|
||||
handle.Instance, handle.Info, verboseLogging);
|
||||
|
||||
ShrinkModOptionalRuntimeIntegration.RefreshGlobalBindings(verboseLogging);
|
||||
}
|
||||
|
||||
private static void DisposeLegacyEventBindings()
|
||||
{
|
||||
foreach (var handle in LoadedMods.Values)
|
||||
{
|
||||
handle.EventBinding?.Dispose();
|
||||
handle.EventBinding = null;
|
||||
EventBus.RemoveBus(ShrinkBusKey.Mod(handle.Info.ModId));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunPhase(IEnumerable<ShrinkModHandle> handles, ShrinkModState targetState, bool verboseLogging,
|
||||
Action<IShrinkMod, ShrinkModContext> callback, string phaseName)
|
||||
{
|
||||
|
||||
@@ -4,14 +4,28 @@ using System.Linq;
|
||||
|
||||
namespace ShrinkModFramework
|
||||
{
|
||||
public interface IReadOnlyShrinkModRegistry<T>
|
||||
{
|
||||
string Name { get; }
|
||||
IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries { get; }
|
||||
IReadOnlyCollection<ShrinkRegistryEntry<T>> BaseEntries { get; }
|
||||
bool Contains(string key);
|
||||
bool TryGet(string key, out T value);
|
||||
bool TryGetEntry(string key, out ShrinkRegistryEntry<T> entry);
|
||||
IReadOnlyList<ShrinkRegistryEntry<T>> GetOverrideCandidates(string key);
|
||||
}
|
||||
|
||||
internal interface IShrinkOwnedRegistry
|
||||
{
|
||||
void RemoveOwnedEntries(string ownerModId);
|
||||
}
|
||||
|
||||
public sealed class ShrinkModRegistry<T> : IShrinkOwnedRegistry
|
||||
internal sealed class ShrinkModRegistry<T> : IReadOnlyShrinkModRegistry<T>, IShrinkOwnedRegistry
|
||||
{
|
||||
private readonly Dictionary<string, ShrinkRegistryEntry<T>> _entries = new();
|
||||
private readonly Dictionary<string, ShrinkRegistryEntry<T>> _baseEntries =
|
||||
new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, List<ShrinkRegistryEntry<T>>> _overrides =
|
||||
new(StringComparer.Ordinal);
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
@@ -23,27 +37,88 @@ namespace ShrinkModFramework
|
||||
Name = name.Trim();
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries => _entries.Values.ToArray();
|
||||
/// <summary>当前生效项;存在覆盖时返回最高优先级的覆盖项。</summary>
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> Entries => _baseEntries.Keys
|
||||
.OrderBy(key => key, StringComparer.Ordinal)
|
||||
.Select(ResolveEntry)
|
||||
.ToArray();
|
||||
|
||||
public void Register(string ownerModId, string key, T value)
|
||||
/// <summary>不应用覆盖的基础注册项快照。</summary>
|
||||
public IReadOnlyCollection<ShrinkRegistryEntry<T>> BaseEntries => _baseEntries.Values
|
||||
.OrderBy(entry => entry.Key, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
private void RegisterBase(string ownerModId, string key, T value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
throw new ArgumentException("归属模组 ID 不能为空。", nameof(ownerModId));
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("注册键不能为空。", nameof(key));
|
||||
|
||||
key = key.Trim();
|
||||
if (_entries.ContainsKey(key))
|
||||
ownerModId = NormalizeOwner(ownerModId);
|
||||
key = NormalizeKey(key);
|
||||
if (_baseEntries.ContainsKey(key))
|
||||
throw new InvalidOperationException($"注册表 '{Name}' 中已存在键 '{key}'。");
|
||||
|
||||
_entries.Add(key, new ShrinkRegistryEntry<T>(ownerModId.Trim(), key, value));
|
||||
_baseEntries.Add(key, new ShrinkRegistryEntry<T>(ownerModId, key, value));
|
||||
}
|
||||
|
||||
public bool Contains(string key) => !string.IsNullOrWhiteSpace(key) && _entries.ContainsKey(key.Trim());
|
||||
/// <summary>
|
||||
/// 使用 owner 作为 namespace 注册内容,并返回最终键。localKey 只能是局部名称,不能包含冒号。
|
||||
/// </summary>
|
||||
public string RegisterNamespaced(string ownerModId, string localKey, T value)
|
||||
{
|
||||
ownerModId = NormalizeOwner(ownerModId);
|
||||
if (ownerModId.IndexOf(':') >= 0)
|
||||
throw new ArgumentException("归属模组 ID 不能包含 ':'。", nameof(ownerModId));
|
||||
if (string.IsNullOrWhiteSpace(localKey))
|
||||
throw new ArgumentException("局部注册键不能为空。", nameof(localKey));
|
||||
|
||||
localKey = localKey.Trim();
|
||||
if (localKey.IndexOf(':') >= 0)
|
||||
throw new ArgumentException("局部注册键不能包含 ':'。", nameof(localKey));
|
||||
|
||||
var key = ownerModId + ":" + localKey;
|
||||
RegisterBase(ownerModId, key, value);
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为已有基础项注册显式覆盖。优先级越高越先命中;同一目标的相同优先级视为硬冲突。
|
||||
/// </summary>
|
||||
public void RegisterOverride(string ownerModId, string targetKey, int priority, T value)
|
||||
{
|
||||
ownerModId = NormalizeOwner(ownerModId);
|
||||
targetKey = NormalizeKey(targetKey);
|
||||
if (!_baseEntries.ContainsKey(targetKey))
|
||||
throw new InvalidOperationException(
|
||||
$"注册表 '{Name}' 的覆盖目标 '{targetKey}' 不存在。");
|
||||
|
||||
if (!_overrides.TryGetValue(targetKey, out var candidates))
|
||||
{
|
||||
candidates = new List<ShrinkRegistryEntry<T>>();
|
||||
_overrides.Add(targetKey, candidates);
|
||||
}
|
||||
|
||||
if (candidates.Any(candidate => candidate.Priority == priority))
|
||||
throw new InvalidOperationException(
|
||||
$"注册表 '{Name}' 的键 '{targetKey}' 在优先级 {priority} 存在覆盖冲突。");
|
||||
|
||||
candidates.Add(ShrinkRegistryEntry<T>.CreateOverride(ownerModId, targetKey, priority, value));
|
||||
}
|
||||
|
||||
public IReadOnlyList<ShrinkRegistryEntry<T>> GetOverrideCandidates(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || !_overrides.TryGetValue(key.Trim(), out var candidates))
|
||||
return Array.Empty<ShrinkRegistryEntry<T>>();
|
||||
|
||||
return candidates
|
||||
.OrderByDescending(candidate => candidate.Priority)
|
||||
.ThenBy(candidate => candidate.OwnerModId, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public bool Contains(string key) =>
|
||||
!string.IsNullOrWhiteSpace(key) && _baseEntries.ContainsKey(key.Trim());
|
||||
|
||||
public bool TryGet(string key, out T value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key) && _entries.TryGetValue(key.Trim(), out var entry))
|
||||
if (TryGetEntry(key, out var entry))
|
||||
{
|
||||
value = entry.Value;
|
||||
return true;
|
||||
@@ -55,8 +130,11 @@ namespace ShrinkModFramework
|
||||
|
||||
public bool TryGetEntry(string key, out ShrinkRegistryEntry<T> entry)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
return _entries.TryGetValue(key.Trim(), out entry);
|
||||
if (!string.IsNullOrWhiteSpace(key) && _baseEntries.ContainsKey(key.Trim()))
|
||||
{
|
||||
entry = ResolveEntry(key.Trim());
|
||||
return true;
|
||||
}
|
||||
|
||||
entry = default;
|
||||
return false;
|
||||
@@ -67,12 +145,52 @@ namespace ShrinkModFramework
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
return;
|
||||
|
||||
var keys = _entries
|
||||
ownerModId = ownerModId.Trim();
|
||||
var keys = _baseEntries
|
||||
.Where(pair => string.Equals(pair.Value.OwnerModId, ownerModId, StringComparison.Ordinal))
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
foreach (var key in keys)
|
||||
_entries.Remove(key);
|
||||
_baseEntries.Remove(key);
|
||||
|
||||
var emptyOverrideTargets = new List<string>();
|
||||
foreach (var pair in _overrides)
|
||||
{
|
||||
pair.Value.RemoveAll(candidate =>
|
||||
string.Equals(candidate.OwnerModId, ownerModId, StringComparison.Ordinal));
|
||||
if (pair.Value.Count == 0)
|
||||
emptyOverrideTargets.Add(pair.Key);
|
||||
}
|
||||
|
||||
foreach (var targetKey in emptyOverrideTargets)
|
||||
_overrides.Remove(targetKey);
|
||||
}
|
||||
|
||||
private ShrinkRegistryEntry<T> ResolveEntry(string key)
|
||||
{
|
||||
if (_overrides.TryGetValue(key, out var candidates) && candidates.Count > 0)
|
||||
{
|
||||
return candidates
|
||||
.OrderByDescending(candidate => candidate.Priority)
|
||||
.ThenBy(candidate => candidate.OwnerModId, StringComparer.Ordinal)
|
||||
.First();
|
||||
}
|
||||
|
||||
return _baseEntries[key];
|
||||
}
|
||||
|
||||
private static string NormalizeOwner(string ownerModId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ownerModId))
|
||||
throw new ArgumentException("归属模组 ID 不能为空。", nameof(ownerModId));
|
||||
return ownerModId.Trim();
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
throw new ArgumentException("注册键不能为空。", nameof(key));
|
||||
return key.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,12 +199,25 @@ namespace ShrinkModFramework
|
||||
public string OwnerModId { get; }
|
||||
public string Key { get; }
|
||||
public T Value { get; }
|
||||
public bool IsOverride { get; }
|
||||
public int Priority { get; }
|
||||
|
||||
public ShrinkRegistryEntry(string ownerModId, string key, T value)
|
||||
: this(ownerModId, key, value, false, 0)
|
||||
{
|
||||
}
|
||||
|
||||
private ShrinkRegistryEntry(string ownerModId, string key, T value, bool isOverride, int priority)
|
||||
{
|
||||
OwnerModId = ownerModId;
|
||||
Key = key;
|
||||
Value = value;
|
||||
IsOverride = isOverride;
|
||||
Priority = priority;
|
||||
}
|
||||
|
||||
internal static ShrinkRegistryEntry<T> CreateOverride(
|
||||
string ownerModId, string key, int priority, T value) =>
|
||||
new(ownerModId, key, value, true, priority);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@ namespace ShrinkModFramework
|
||||
{
|
||||
private readonly Dictionary<string, object> _registries = new();
|
||||
|
||||
public ShrinkModRegistry<T> GetOrCreateRegistry<T>(string name)
|
||||
public IReadOnlyShrinkModRegistry<T> GetOrCreateRegistry<T>(string name) =>
|
||||
GetOrCreateMutableRegistry<T>(name);
|
||||
|
||||
internal ShrinkModRegistry<T> GetOrCreateMutableRegistry<T>(string name)
|
||||
{
|
||||
var compositeKey = BuildCompositeKey(typeof(T), name);
|
||||
if (_registries.TryGetValue(compositeKey, out var existing))
|
||||
@@ -18,7 +21,7 @@ namespace ShrinkModFramework
|
||||
return registry;
|
||||
}
|
||||
|
||||
public bool TryGetRegistry<T>(string name, out ShrinkModRegistry<T> registry)
|
||||
public bool TryGetRegistry<T>(string name, out IReadOnlyShrinkModRegistry<T> registry)
|
||||
{
|
||||
var compositeKey = BuildCompositeKey(typeof(T), name);
|
||||
if (_registries.TryGetValue(compositeKey, out var existing))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"references": [
|
||||
"Newtonsoft.Json",
|
||||
"ShrinkContext.Core.Runtime",
|
||||
"ShrinkEventBus.Runtime",
|
||||
"UniTask"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
|
||||
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user