#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 _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 Factory = null!; } private static readonly object Gate = new(); private static readonly List Entries = new(); private static readonly Dictionary Bindings = new(); private static long _nextId; public static void Register(ShrinkBusKey key, Func 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(string bus, Action 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(string bus, ShrinkAsyncEventHandler 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(string bus, Func 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 _buses = new(); private bool _disposed; public IEnumerable 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 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(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus, string? configuredBus, object? owner, Action 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(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus, string? configuredBus, object? owner, ShrinkAsyncEventHandler 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(IShrinkBusResolver resolver, ShrinkBusKey? defaultBus, string? configuredBus, object? owner, Func handler, ShrinkEventPriority priority, int numericPriority, bool receiveCanceled) where TEvent : IShrinkEvent { if (handler == null) throw new ArgumentNullException(nameof(handler)); return SubscribeAsync(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); } } }