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:
2026-08-26 01:15:48 +08:00
parent 67d32795c4
commit ad5a7b68a3
129 changed files with 5071 additions and 5584 deletions
@@ -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
+163 -96
View File
@@ -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(" 基准对象:独立实例 busAllowPerPhaseDispatch 已开启)");
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,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,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);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f19286886fb64b3ba7bfbd010c540913
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -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;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ffae3efb7664efcb82f2bbefbdd5a4e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
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: