#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 Buses = new(); internal static readonly IShrinkBusResolver Resolver = new GlobalBusResolver(); private static readonly IShrinkEventBus DefaultBus = CreateDefaultBus(); private static Action? _posted; private static Action? _detailedPosted; internal static event Action Posted { add { _posted += value; } remove { _posted -= value; } } internal static bool HasPostedObservers { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => Volatile.Read(ref _posted) != null; } internal static event Action 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 IShrinkEventBus CreateBus(ShrinkBusKey key, ShrinkBusOptions options) { 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; } public static IShrinkEventBus GetOrCreateBus(ShrinkBusKey key, ShrinkBusOptions options) { 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 bool TryGetBus(ShrinkBusKey key, out IShrinkEventBus bus) => Buses.TryGetValue(key, out bus!); public static bool RemoveBus(ShrinkBusKey key) { if (key == ShrinkBusKey.Game || !Buses.TryRemove(key, out var bus)) return false; ShrinkStaticBindingRegistry.DetachBus(key); bus.Dispose(); return true; } public static ShrinkPostResult Post(in TEvent eventData) where TEvent : IShrinkEvent => DefaultBus.Post(in eventData); public static UniTask PostAsync(TEvent eventData, CancellationToken cancellationToken = default) where TEvent : IShrinkEvent => DefaultBus.PostAsync(eventData, cancellationToken); 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); } 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 = 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) { #if UNITY_5_3_OR_NEWER UnityEngine.Debug.LogException(exception); #else System.Diagnostics.Trace.TraceError(exception.ToString()); #endif } } } }