#nullable enable using System; using System.Runtime.CompilerServices; namespace ShrinkEventBus { /// Marker shared by managed, Unity and ECS event payloads. public interface IShrinkEvent { } public interface IShrinkCancelableEvent : IShrinkEvent { bool IsCanceled { get; } void SetCanceled(bool value); } public interface IShrinkResultEvent : 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 { 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; } } }