diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f15f8a4
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*.cs]
+dotnet_diagnostic.IDE0290.severity = none
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 34b39ed..358addf 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,21 +3,23 @@
true
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/StarMap.API/Events/IEventPublisher.cs b/StarMap.API/Events/IEventPublisher.cs
new file mode 100644
index 0000000..8145171
--- /dev/null
+++ b/StarMap.API/Events/IEventPublisher.cs
@@ -0,0 +1,7 @@
+namespace StarMap.API.Events
+{
+ public interface IEventPublisher : IDisposable
+ {
+ void Publish(T message);
+ }
+}
diff --git a/StarMap.API/Events/ISubscription.cs b/StarMap.API/Events/ISubscription.cs
new file mode 100644
index 0000000..45e277a
--- /dev/null
+++ b/StarMap.API/Events/ISubscription.cs
@@ -0,0 +1,6 @@
+namespace StarMap.API.Events
+{
+ public interface ISubscription : IDisposable
+ {
+ }
+}
diff --git a/StarMap.API/Mediation/IEventPublisher.cs b/StarMap.API/Mediation/IEventPublisher.cs
deleted file mode 100644
index 6a2f057..0000000
--- a/StarMap.API/Mediation/IEventPublisher.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace StarMap.API.Mediation
-{
- public interface IEventPublisher : IDisposable where T : IStarMapEvent
- {
- void Publish(T message);
- Task PublishParallel(T message);
- }
-}
diff --git a/StarMap.API/Mediation/IStarMapEvent.cs b/StarMap.API/Mediation/IStarMapEvent.cs
deleted file mode 100644
index fbbfbf5..0000000
--- a/StarMap.API/Mediation/IStarMapEvent.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace StarMap.API.Mediation
-{
- public interface IStarMapEvent
- {
- }
-}
diff --git a/StarMap.API/Mediation/ISubscription.cs b/StarMap.API/Mediation/ISubscription.cs
deleted file mode 100644
index 47f02d1..0000000
--- a/StarMap.API/Mediation/ISubscription.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace StarMap.API.Mediation
-{
- public interface ISubscription : IDisposable where T : IStarMapEvent
- {
- }
-}
diff --git a/StarMap.API/Services/IEventBus.cs b/StarMap.API/Services/IEventBus.cs
new file mode 100644
index 0000000..fa9432e
--- /dev/null
+++ b/StarMap.API/Services/IEventBus.cs
@@ -0,0 +1,13 @@
+using StarMap.API.Events;
+
+namespace StarMap.API.Services
+{
+ public interface IEventBus
+ {
+ IEventPublisher GetPublisher();
+ IEventPublisher GetPublisher(string eventName);
+
+ ISubscription Subscribe(Action handler);
+ ISubscription Subscribe(Action handler, string eventName);
+ }
+}
diff --git a/StarMap.API/Services/IMediationService.cs b/StarMap.API/Services/IMediationService.cs
deleted file mode 100644
index d8f3450..0000000
--- a/StarMap.API/Services/IMediationService.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using StarMap.API.Mediation;
-
-namespace StarMap.API.Services
-{
- public interface IMediationService
- {
- ISubscription Subscribe(Action handler) where T : IStarMapEvent;
- IEventPublisher RegisterPublisher() where T : IStarMapEvent;
- }
-}
diff --git a/StarMap.Core/API/EventBus.cs b/StarMap.Core/API/EventBus.cs
new file mode 100644
index 0000000..5285b5b
--- /dev/null
+++ b/StarMap.Core/API/EventBus.cs
@@ -0,0 +1,39 @@
+using StarMap.API.Events;
+using StarMap.API.Services;
+using StarMap.Core.ModRepository;
+using StarMap.Core.Services;
+
+namespace StarMap.Core.API
+{
+ internal class EventBus : IEventBus
+ {
+ private readonly RuntimeMod _mod;
+ private readonly IMediator _mediator;
+
+ public EventBus(IModContext modContext, IMediator mediator)
+ {
+ _mod = modContext.Mod;
+ _mediator = mediator;
+ }
+
+ public IEventPublisher GetPublisher()
+ {
+ return _mediator.GetPublisher();
+ }
+
+ public IEventPublisher GetPublisher(string eventName)
+ {
+ return _mediator.GetPublisher(eventName);
+ }
+
+ public ISubscription Subscribe(Action handler)
+ {
+ return _mediator.Subscribe(_mod, handler);
+ }
+
+ public ISubscription Subscribe(Action handler, string eventName)
+ {
+ return _mediator.Subscribe(_mod, handler, eventName);
+ }
+ }
+}
diff --git a/StarMap.Core/API/MediationService.cs b/StarMap.Core/API/MediationService.cs
deleted file mode 100644
index 4767293..0000000
--- a/StarMap.Core/API/MediationService.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using StarMap.API.Mediation;
-using StarMap.API.Services;
-using StarMap.Core.ModRepository;
-using StarMap.Core.Services;
-
-namespace StarMap.Core.API
-{
- internal class MediationService : IMediationService
- {
- private readonly RuntimeMod _mod;
- private readonly IMediator _mediator;
-
- public MediationService(IModContext modContext, IMediator mediator)
- {
- _mod = modContext.Mod;
- _mediator = mediator;
- }
-
- public IEventPublisher RegisterPublisher() where T : IStarMapEvent
- {
- return _mediator.RegisterPublisher(_mod);
- }
-
- public ISubscription Subscribe(Action handler) where T : IStarMapEvent
- {
- return _mediator.Subscribe(handler);
- }
- }
-}
diff --git a/StarMap.Core/ModRepository/ModsLoader.cs b/StarMap.Core/ModRepository/ModsLoader.cs
index 42a1ff4..99dde1e 100644
--- a/StarMap.Core/ModRepository/ModsLoader.cs
+++ b/StarMap.Core/ModRepository/ModsLoader.cs
@@ -18,6 +18,7 @@ internal sealed class ModsLoader : IModsLoader
private readonly AssemblyLoadContext _coreAssemblyLoadContext;
private readonly IPatchManager _patchManager;
private readonly IServiceProviderService _serviceProviderService;
+ private readonly ILogger _logger;
public static Dictionary RegisteredMethodAttributes = [];
@@ -31,13 +32,14 @@ internal sealed class ModsLoader : IModsLoader
return (attrType.Name, attrObject);
}
- public ModsLoader([FromKeyedServices("Core")] AssemblyLoadContext coreAssemblyLoadContext, IPatchManager patchManager, IServiceProviderService serviceProviderService)
+ public ModsLoader([FromKeyedServices("Core")] AssemblyLoadContext coreAssemblyLoadContext, IPatchManager patchManager, IServiceProviderService serviceProviderService, ILogger logger)
{
_coreAssemblyLoadContext = coreAssemblyLoadContext;
_patchManager = patchManager;
_serviceProviderService = serviceProviderService;
+ _logger = logger;
- Assembly coreAssembly = typeof(StarMapModAttribute).Assembly;
+ var coreAssembly = typeof(StarMapModAttribute).Assembly;
RegisteredMethodAttributes = coreAssembly
.GetTypes()
@@ -70,7 +72,7 @@ private void PrepareMods()
{
if (!mod.Enabled)
{
- Console.WriteLine($"StarMap - Not loading mod: {mod.Id} because it is disabled in manifest");
+ _logger.Information($"StarMap - Not loading mod: {mod.Id} because it is disabled in manifest");
continue;
}
@@ -81,17 +83,17 @@ private void PrepareMods()
if (!runtimeMod.AllDependenciesLoaded(ModRegistry))
{
- Console.WriteLine($"StarMap - Delaying load of mod: {runtimeMod.ModId} due to missing dependencies: {string.Join(", ", runtimeMod.NotLoadedModDependencies.Keys)}");
+ _logger.Information($"StarMap - Delaying load of mod: {runtimeMod.ModId} due to missing dependencies: {string.Join(", ", runtimeMod.NotLoadedModDependencies.Keys)}");
continue;
}
if (!runtimeMod.InitializeMod(ModRegistry, _patchManager, _serviceProviderService))
{
- Console.WriteLine($"StarMap - Failed to initialize mod: {runtimeMod.ModId} from manifest");
+ _logger.Warning($"StarMap - Failed to initialize mod: {runtimeMod.ModId} from manifest");
continue;
}
- Console.WriteLine($"StarMap - Loaded mod: {runtimeMod.ModId} from manifest");
+ _logger.Information($"StarMap - Loaded mod: {runtimeMod.ModId} from manifest");
var dependentMods = runtimeMod.CheckForDependentMods(ModRegistry);
@@ -99,11 +101,11 @@ private void PrepareMods()
{
if (dependentMod.InitializeMod(ModRegistry, _patchManager, _serviceProviderService))
{
- Console.WriteLine($"StarMap - Loaded mod: {dependentMod.ModId} after loading {runtimeMod.ModId}");
+ _logger.Information($"StarMap - Loaded mod: {dependentMod.ModId} after loading {runtimeMod.ModId}");
}
else
{
- Console.WriteLine($"StarMap - Failed to load mod: {dependentMod.ModId} after loading {runtimeMod.ModId}");
+ _logger.Warning($"StarMap - Failed to load mod: {dependentMod.ModId} after loading {runtimeMod.ModId}");
}
}
}
@@ -127,11 +129,11 @@ private void TryLoadWaitingMods()
if (waitingMod.InitializeMod(ModRegistry, _patchManager, _serviceProviderService))
{
- Console.WriteLine($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}");
+ _logger.Information($"StarMap - Loaded mod: {waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}");
}
else
{
- Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}");
+ _logger.Warning($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, not loaded optional mods: {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}");
}
waitingMod.NotLoadedModDependencies.Clear();
}
@@ -142,7 +144,7 @@ private void TryLoadWaitingMods()
{
foreach (var waitingMod in ModRegistry.WaitingMods)
{
- Console.WriteLine($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}");
+ _logger.Warning($"StarMap - Failed to load mod:{waitingMod.ModId} after all mods were loaded, missing mods (some may be optional): {string.Join(",", waitingMod.NotLoadedModDependencies.Values.Select(mod => mod.ModId))}");
}
ModRegistry.WaitingMods.Clear();
}
diff --git a/StarMap.Core/Patches/StarMapPatches.cs b/StarMap.Core/Patches/StarMapPatches.cs
index 05065a2..58a9f6d 100644
--- a/StarMap.Core/Patches/StarMapPatches.cs
+++ b/StarMap.Core/Patches/StarMapPatches.cs
@@ -1,6 +1,7 @@
using HarmonyLib;
using KSA;
using StarMap.API.Services;
+using StarMap.Core.Services;
using StarMap.Core.UI.ConfirmRestart;
using System.Diagnostics;
@@ -14,10 +15,12 @@ internal interface IStarMapPatches
internal class StarMapPatches : IStarMapPatches
{
private readonly IPatchManager _patchManager;
+ private readonly ILogger _logger;
- public StarMapPatches(IPatchManager patchManager)
+ public StarMapPatches(IPatchManager patchManager, ILogger logger)
{
_patchManager = patchManager;
+ _logger = logger;
}
public void Init()
@@ -50,13 +53,13 @@ static bool wereNewModsEnabled()
if (confirmRestart.Restart)
{
- Console.WriteLine("StarMap - RESTARTING");
- Console.WriteLine("======================================");
+ _logger.Information("StarMap - RESTARTING");
+ _logger.Information("======================================");
var mainModule = Process.GetCurrentProcess().MainModule;
if (mainModule is null || Path.GetDirectoryName(mainModule.FileName) is not string starMapInstallLocation)
{
- Console.WriteLine("Unable to get StarMap install location for restart.");
+ _logger.Warning("Unable to get StarMap install location for restart.");
}
else
{
diff --git a/StarMap.Core/Services/Logger.cs b/StarMap.Core/Services/Logger.cs
new file mode 100644
index 0000000..11614a7
--- /dev/null
+++ b/StarMap.Core/Services/Logger.cs
@@ -0,0 +1,37 @@
+namespace StarMap.Core.Services
+{
+ internal interface ILogger
+ {
+ void Information(string message);
+ void Warning(string message);
+ void Error(string message);
+ }
+
+ internal class Logger : ILogger
+ {
+ //private readonly LogCategory _category;
+
+ public Logger()
+ {
+ //_category = LogCategory.Make("StarMap");
+ }
+
+ public void Error(string message)
+ {
+ Console.WriteLine($"StarMap error: '{message}'");
+ //_category.Error(message);
+ }
+
+ public void Information(string message)
+ {
+ Console.WriteLine($"StarMap info: '{message}'");
+ //_category.Info(message);
+ }
+
+ public void Warning(string message)
+ {
+ Console.WriteLine($"StarMap warning: '{message}'");
+ //_category.Warning(message);
+ }
+ }
+}
diff --git a/StarMap.Core/Services/Mediator.cs b/StarMap.Core/Services/Mediator.cs
index b760c3c..5bcc878 100644
--- a/StarMap.Core/Services/Mediator.cs
+++ b/StarMap.Core/Services/Mediator.cs
@@ -1,154 +1,202 @@
-using StarMap.API.Mediation;
+using StarMap.API.Events;
using StarMap.Core.ModRepository;
using System.Collections.Concurrent;
-using System.Runtime.Loader;
namespace StarMap.Core.Services
{
internal interface IMediator
{
- IEventPublisher RegisterPublisher(RuntimeMod mod) where T : IStarMapEvent;
- ISubscription Subscribe(Action action) where T : IStarMapEvent;
- }
+ IEventPublisher GetPublisher();
+ IEventPublisher GetPublisher(string eventName);
- public interface IEventPublisher { }
+ ISubscription Subscribe(RuntimeMod mod, Action handler);
+ ISubscription Subscribe(RuntimeMod mod, Action handler, string eventName);
+ }
- internal class SubscriptionHandle : ISubscription where T : IStarMapEvent
+ internal class Mediator(ILogger logger) : IMediator
{
- private readonly EventPublisher _publisher;
+ private interface IEventBus { }
- private readonly Action _action;
+ private readonly ConcurrentDictionary _busses = [];
- public SubscriptionHandle(EventPublisher publisher, Action action)
+ private TypeEventBus GetTypeBus()
{
- _publisher = publisher;
- _action = action;
+ var bus = _busses.GetOrAdd(typeof(T), _ => new TypeEventBus(logger));
+
+ if (bus is TypeEventBus typeBus)
+ return typeBus;
+
+ throw new InvalidOperationException("Something went wrong in bus management");
}
- public void Invoke(T message)
+ public IEventPublisher GetPublisher()
{
- _action.Invoke(message);
+ return GetTypeBus().CreatePublisher();
}
- public void Dispose()
+ public IEventPublisher GetPublisher(string eventName)
{
- _publisher.Unsubscribe(this);
+ return GetTypeBus().GetNamedBus(eventName).CreatePublisher();
}
- }
-
- internal class EventPublisher(Mediator mediator) : IEventPublisher, IEventPublisher where T : IStarMapEvent
- {
- private readonly Lock _lock = new();
- private bool _isDisposed;
+ public ISubscription Subscribe(RuntimeMod mod, Action handler)
+ {
+ return GetTypeBus().Subscribe(mod, handler);
+ }
- private readonly HashSet> _handlers = [];
+ public ISubscription Subscribe(RuntimeMod mod, Action handler, string eventName)
+ {
+ return GetTypeBus().GetNamedBus(eventName).Subscribe(mod, handler);
+ }
- public ISubscription Subscribe(Action action)
+ private class TypeEventBus : EventBus
{
- var handle = new SubscriptionHandle(this, action);
+ public TypeEventBus(ILogger logger) : base(logger) { }
+
+ private readonly ConcurrentDictionary> _namedBusses = [];
- lock (_lock)
+ public EventBus GetNamedBus(string eventName)
{
- ObjectDisposedException.ThrowIf(_isDisposed, this);
- _handlers.Add(handle);
+ return _namedBusses.GetOrAdd(eventName, (_) => new NamedEventBus(_logger, eventName));
}
- return handle;
+ protected override string GetBusSpecificExceptionPart()
+ {
+ return $"event for type {typeof(T)}";
+ }
}
- public void Unsubscribe(SubscriptionHandle handle)
+ private class NamedEventBus : EventBus
{
- lock (_lock)
- {
- if (_isDisposed)
- return;
+ private readonly string _eventName;
- _handlers.Remove(handle);
+ public NamedEventBus(ILogger logger, string eventName) : base(logger)
+ {
+ _eventName = eventName;
}
- }
- public void Publish(T message)
- {
- foreach (var handler in GetHandlersSnapshot())
+ protected override string GetBusSpecificExceptionPart()
{
- handler.Invoke(message);
+ return $"event '{_eventName}' for type {typeof(T)}";
}
}
- public Task PublishParallel(T message)
+ private abstract class EventBus : IEventBus
{
- var tasks = new List();
+ private readonly Lock _lock = new();
+
+ protected readonly ILogger _logger;
- foreach (var handler in GetHandlersSnapshot())
+ public EventBus(ILogger logger)
{
- tasks.Add(Task.Run(() => handler.Invoke(message)));
+ _logger = logger;
}
- return Task.WhenAll(tasks);
- }
+ private HashSet> _subscription = [];
+ private HashSet> _publishers = [];
- private SubscriptionHandle[] GetHandlersSnapshot()
- {
- lock (_lock)
+ public IEventPublisher CreatePublisher()
{
- if (_isDisposed)
- return [];
+ lock (_lock)
+ {
+ var publisher = new Publisher(this);
+ _publishers.Add(publisher);
+ return publisher;
+ }
+ }
- return [.. _handlers];
+ public void RemovePublisher(IEventPublisher publisher)
+ {
+ lock (_lock)
+ {
+ _publishers.Remove(publisher);
+ }
}
- }
- public void Dispose()
- {
- lock (_lock)
+ public ISubscription Subscribe(RuntimeMod mod, Action action)
{
- if (_isDisposed)
- return;
+ lock (_lock)
+ {
+ var subHandle = new SubscriptionHandle(mod, this, action);
+ _subscription.Add(subHandle);
+ return subHandle;
+ }
+ }
- _isDisposed = true;
+ public void Unsubscribe(SubscriptionHandle handle)
+ {
+ lock (_lock)
+ {
+ _subscription.Remove(handle);
+ }
+ }
- mediator.UnregisterPublisher(this);
- _handlers.Clear();
+ public void Publish(T message)
+ {
+ SubscriptionHandle[] subscriptions;
+
+ lock (_lock)
+ {
+ subscriptions = [.. _subscription];
+ }
+
+ foreach (var sub in subscriptions)
+ {
+ try
+ {
+ sub.Invoke(message);
+ }
+ catch (Exception ex)
+ {
+ _logger.Warning($"Mod '{sub.Mod.ModId}' could not handle event from bus: {GetBusSpecificExceptionPart()}\n {ex.Message}");
+ }
+ }
}
+ protected abstract string GetBusSpecificExceptionPart();
}
- }
-
- internal class Mediator : IMediator
- {
- private readonly ConcurrentDictionary _publishers = [];
- public IEventPublisher RegisterPublisher(RuntimeMod mod) where T : IStarMapEvent
+ private class Publisher(EventBus bus) : IEventPublisher
{
- var type = typeof(T);
- if (AssemblyLoadContext.GetLoadContext(typeof(T).Assembly) != mod.ModAssemblyLoadContext)
- throw new Exception("Events are only allowed to be published by the owner of the event type");
+ private bool _isDisposed;
- var publisher = new EventPublisher(this);
+ public void Publish(T message)
+ {
+ ObjectDisposedException.ThrowIf(Volatile.Read(ref _isDisposed), this);
+ bus.Publish(message);
+ }
- if (!_publishers.TryAdd(type, publisher))
- throw new Exception($"A publisher for type '{typeof(T)}' was already registered");
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _isDisposed, true))
+ return;
- return publisher;
+ bus.RemovePublisher(this);
+ }
}
- public void UnregisterPublisher(IEventPublisher publisher)
+ private class SubscriptionHandle : ISubscription
{
- _publishers.TryRemove(new(typeof(T), publisher));
- }
+ public RuntimeMod Mod { get; }
+ private readonly EventBus _bus;
+ private readonly Action _action;
- public ISubscription Subscribe(Action action) where T : IStarMapEvent
- {
- if (!_publishers.TryGetValue(typeof(T), out var publisher))
+ public SubscriptionHandle(RuntimeMod mod, EventBus bus, Action action)
{
- throw new Exception("A publisher for this event was not found");
+ Mod = mod;
+ _bus = bus;
+ _action = action;
}
- if (publisher is not EventPublisher typedPublisher)
- throw new Exception("Something went wrong in the type of the publisher");
+ public void Invoke(T message)
+ {
+ _action.Invoke(message);
+ }
- return typedPublisher.Subscribe(action);
+ public void Dispose()
+ {
+ _bus.Unsubscribe(this);
+ }
}
}
}
diff --git a/StarMap.Core/StarMap.Core.csproj b/StarMap.Core/StarMap.Core.csproj
index d132d7f..55e596b 100644
--- a/StarMap.Core/StarMap.Core.csproj
+++ b/StarMap.Core/StarMap.Core.csproj
@@ -9,6 +9,8 @@
+
+
diff --git a/StarMap.Core/StarMapCore.cs b/StarMap.Core/StarMapCore.cs
index 45d6855..9f3f697 100644
--- a/StarMap.Core/StarMapCore.cs
+++ b/StarMap.Core/StarMapCore.cs
@@ -18,6 +18,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddKeyedSingleton("Core", coreAssemblyLoadContext);
+ serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
serviceCollection.AddSingleton();
@@ -29,7 +30,7 @@ public StarMapCore(AssemblyLoadContext coreAssemblyLoadContext)
serviceCollection.AddScoped();
serviceCollection.AddScoped();
serviceCollection.AddScoped();
- serviceCollection.AddScoped();
+ serviceCollection.AddScoped();
serviceCollection.AddScoped();
serviceCollection.AddSingleton();
diff --git a/docs/modding/constructor-injection.md b/docs/modding/constructor-injection.md
index e24e43c..6bea8ac 100644
--- a/docs/modding/constructor-injection.md
+++ b/docs/modding/constructor-injection.md
@@ -6,7 +6,7 @@ The services currently registered in that scope are:
- `IModServiceRegistry` for registering and retrieving services shared between mods
- `IModMenuService` for adding items to KSA's **Mods** menu
-- `IMediationService` for publishing and subscribing to typed events
+- `IEventBus` for publishing and subscribing to typed messages
- `IPatchService` for registering method patches
## Request services
@@ -34,4 +34,3 @@ Use one public constructor, and ensure its parameters can be resolved from the s
Constructor injection applies only to the `[StarMapMod]` entry class. Lifecycle callbacks are called with their documented fixed signatures; StarMap does not inject their parameters. See [lifecycle callbacks](lifecycle.md).
Each mod has its own dependency-injection scope. Keep injected services on the entry class instance, and dispose handles returned by services, such as menu items, event publishers, and event subscriptions, in `[StarMapUnload]`. For cross-mod service contracts and registration, see [mod-provided services](mod-services.md).
-
diff --git a/docs/modding/dependencies.md b/docs/modding/dependencies.md
index d422c9a..ea5bde5 100644
--- a/docs/modding/dependencies.md
+++ b/docs/modding/dependencies.md
@@ -42,9 +42,9 @@ StarMap loads each mod in a separate assembly load context. Declare inter-mod re
## Sharing event types between mods
-When using [`IMediationService`](event-mediation.md), the event type must be defined in an assembly loaded by the publishing mod and shared with each subscribing mod. Add that assembly to the provider's `ExportedAssemblies` and to the consumer's `ImportedAssemblies`; make the provider a required dependency if the consumer subscribes during startup.
+When using the [typed event bus](event-bus.md), every mod publishing or subscribing to a cross-mod message must use the same runtime type from a shared assembly. Add the contract assembly to the provider's `ExportedAssemblies` and to each dependent mod's `ImportedAssemblies`. Declare the provider as a dependency when its contract assembly is needed, and make it required if the mod cannot function without that assembly. A subscription does not require a publisher to be created first.
-The consumer also needs a compile-time reference to the provider's event contract assembly. See [typed event mediation](event-mediation.md) for publisher and subscriber examples.
+Each participating mod also needs a compile-time reference to the contract assembly. See the [typed event bus guide](event-bus.md) for publisher and subscriber examples.
## Sharing mod service contracts
diff --git a/docs/modding/event-bus.md b/docs/modding/event-bus.md
new file mode 100644
index 0000000..5b83324
--- /dev/null
+++ b/docs/modding/event-bus.md
@@ -0,0 +1,109 @@
+# Typed event bus
+
+`IEventBus` lets mods publish and subscribe to typed messages. The bus matches messages by their type; no marker interface is required:
+
+```csharp
+public sealed record SelectionChanged(string Selection);
+```
+
+## Publishing a message
+
+Inject `IEventBus` into the `[StarMapMod]` entry class and get a publisher for the message type:
+
+```csharp
+using StarMap.API;
+using StarMap.API.Events;
+using StarMap.API.Services;
+
+[StarMapMod]
+public sealed class SelectionPublisher
+{
+ private readonly IEventBus _events;
+ private IEventPublisher? _publisher;
+
+ public SelectionPublisher(IEventBus events)
+ {
+ _events = events;
+ }
+
+ [StarMapBeforeMain]
+ public void Initialize()
+ {
+ _publisher = _events.GetPublisher();
+ }
+
+ public void ChangeSelection(string selection)
+ {
+ _publisher?.Publish(new SelectionChanged(selection));
+ }
+
+ [StarMapUnload]
+ public void Unload()
+ {
+ _publisher?.Dispose();
+ }
+}
+```
+
+## Subscribing to a message
+
+Subscribe with the same message type. A subscription can be created before any publisher:
+
+```csharp
+using System;
+using StarMap.API;
+using StarMap.API.Events;
+using StarMap.API.Services;
+
+[StarMapMod]
+public sealed class SelectionSubscriber
+{
+ private readonly IEventBus _events;
+ private ISubscription? _subscription;
+
+ public SelectionSubscriber(IEventBus events)
+ {
+ _events = events;
+ }
+
+ [StarMapBeforeMain]
+ public void Initialize()
+ {
+ _subscription = _events.Subscribe(OnSelectionChanged);
+ }
+
+ private void OnSelectionChanged(SelectionChanged message)
+ {
+ Console.WriteLine($"Selection changed to {message.Selection}");
+ }
+
+ [StarMapUnload]
+ public void Unload()
+ {
+ _subscription?.Dispose();
+ }
+}
+```
+
+Keep and dispose publisher and subscription handles when they are no longer needed, usually in `[StarMapUnload]`. Calling `Publish` on a disposed publisher throws `ObjectDisposedException`. The bus does not queue or replay messages sent before a subscription is added.
+
+## Named channels
+
+The overloads with an `eventName` string create a separate channel for that message type. The publisher and subscribers must use the same type and name:
+
+```csharp
+_publisher = _events.GetPublisher("selection-panel");
+_subscription = _events.Subscribe(
+ OnSelectionChanged,
+ "selection-panel");
+```
+
+Named channels are scoped to the message type; the same name used with a different type is a different channel. Calls without an event name use the default channel and do not communicate with named channels.
+
+## Message delivery
+
+`Publish` invokes the current subscribers synchronously on the calling thread. Handlers for one publish call run one at a time, but their order is not guaranteed. There is no parallel-publish or worker-thread dispatch API. If a handler throws, the bus logs the exception and continues delivering the message to the remaining subscribers. Concurrent publish calls can invoke the same handler concurrently, so protect shared state as needed.
+
+## Sharing message types between mods
+
+Every mod publishing or subscribing to a cross-mod message must use the same runtime type from a shared assembly. Put message contracts in an assembly exported by the mod that provides them and imported by every other mod that uses those types. Each mod using the messages also needs a compile-time reference to the contract assembly. See [mod dependencies](dependencies.md#sharing-event-types-between-mods) for the configuration example.
diff --git a/docs/modding/event-mediation.md b/docs/modding/event-mediation.md
deleted file mode 100644
index 192a623..0000000
--- a/docs/modding/event-mediation.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# Typed event mediation
-
-`IMediationService` lets mods publish and subscribe to typed events. Event types implement `IStarMapEvent`. Define an event type in an assembly loaded by the publishing mod:
-
-```csharp
-using StarMap.API.Mediation;
-
-public sealed record SelectionChanged(string Selection) : IStarMapEvent;
-```
-
-## Publishing an event
-
-The mod that owns the event type registers its publisher once, then publishes messages:
-
-```csharp
-using StarMap.API;
-using StarMap.API.Mediation;
-using StarMap.API.Services;
-
-[StarMapMod]
-public sealed class SelectionPublisher
-{
- private readonly IMediationService _events;
- private IEventPublisher? _publisher;
-
- public SelectionPublisher(IMediationService events)
- {
- _events = events;
- }
-
- [StarMapBeforeMain]
- public void Initialize()
- {
- _publisher = _events.RegisterPublisher();
- }
-
- public void ChangeSelection(string selection)
- {
- _publisher?.Publish(new SelectionChanged(selection));
- }
-
- [StarMapUnload]
- public void Unload()
- {
- _publisher?.Dispose();
- }
-}
-```
-
-Only the mod that owns the event type's assembly can register its publisher, and only one publisher can be registered for each event type. Attempting to register another publisher throws.
-
-## Subscribing to an event
-
-A dependent mod can subscribe after the publisher has registered:
-
-```csharp
-using System;
-using StarMap.API;
-using StarMap.API.Mediation;
-using StarMap.API.Services;
-
-[StarMapMod]
-public sealed class SelectionSubscriber
-{
- private readonly IMediationService _events;
- private ISubscription? _subscription;
-
- public SelectionSubscriber(IMediationService events)
- {
- _events = events;
- }
-
- [StarMapBeforeMain]
- public void Initialize()
- {
- _subscription = _events.Subscribe(OnSelectionChanged);
- }
-
- private void OnSelectionChanged(SelectionChanged message)
- {
- Console.WriteLine($"Selection changed to {message.Selection}");
- }
-
- [StarMapUnload]
- public void Unload()
- {
- _subscription?.Dispose();
- }
-}
-```
-
-`Subscribe()` throws if a publisher for `T` has not been registered yet. Dispose subscriptions and the publisher when their respective mods unload.
-
-## Synchronous and parallel publishing
-
-`Publish` invokes subscribers synchronously. `PublishParallel` runs each handler concurrently and returns a `Task` that completes when the handlers finish. Handlers used with `PublishParallel` must be thread-safe and should not access game state unless that access is safe from a worker thread.
-
-## Sharing event types between mods
-
-The event type's assembly must be shared with each subscribing mod so both mods use the same event type. Add it to the provider's `ExportedAssemblies` and the consumer's `ImportedAssemblies`. The consumer must also declare the provider as a required dependency if it subscribes during startup. See [mod dependencies](dependencies.md#sharing-event-types-between-mods) for the configuration example.
diff --git a/docs/modding/index.md b/docs/modding/index.md
index 1c7922d..81c7629 100644
--- a/docs/modding/index.md
+++ b/docs/modding/index.md
@@ -15,7 +15,7 @@ HelloStarMap/
`-- any private dependency assemblies
```
-Start with the [first-mod walkthrough](first-mod.md). Then read about [lifecycle callbacks](lifecycle.md), [services and mechanics](services.md), and [mod dependencies](dependencies.md). The service overview links to separate guides for [constructor injection](constructor-injection.md), [mod-provided services](mod-services.md), [menu items](mod-menu.md), [event mediation](event-mediation.md), and [method patching](patching.md).
+Start with the [first-mod walkthrough](first-mod.md). Then read about [lifecycle callbacks](lifecycle.md), [services and mechanics](services.md), and [mod dependencies](dependencies.md). The service overview links to separate guides for [constructor injection](constructor-injection.md), [mod-provided services](mod-services.md), [menu items](mod-menu.md), the [typed event bus](event-bus.md), and [method patching](patching.md).
## Entry assembly and mod ID
diff --git a/docs/modding/lifecycle.md b/docs/modding/lifecycle.md
index 3fe00f1..70c9dcd 100644
--- a/docs/modding/lifecycle.md
+++ b/docs/modding/lifecycle.md
@@ -18,4 +18,4 @@ The callback methods return `void`. Use the exact parameter types and counts; me
## Constructor-injected services
-StarMap provides `IModServiceRegistry`, `IModMenuService`, `IMediationService`, and `IPatchService` through the entry class constructor. See [constructor injection](constructor-injection.md) for the injection mechanism and the individual pages for [mod-provided services](mod-services.md), [menu items](mod-menu.md), [event mediation](event-mediation.md), and [method patching](patching.md) for their usage.
+StarMap provides `IModServiceRegistry`, `IModMenuService`, `IEventBus`, and `IPatchService` through the entry class constructor. See [constructor injection](constructor-injection.md) for the injection mechanism and the individual pages for [mod-provided services](mod-services.md), [menu items](mod-menu.md), the [typed event bus](event-bus.md), and [method patching](patching.md) for their usage.
diff --git a/docs/modding/mod-services.md b/docs/modding/mod-services.md
index 91c2185..d0e08b0 100644
--- a/docs/modding/mod-services.md
+++ b/docs/modding/mod-services.md
@@ -1,6 +1,6 @@
# Mod-provided services
-`IModServiceRegistry` lets a provider mod expose an object through a shared service contract so dependent mods can use it. This is separate from StarMap's built-in services such as `IModMenuService` and `IMediationService`.
+`IModServiceRegistry` lets a provider mod expose an object through a shared service contract so dependent mods can use it. This is separate from StarMap's built-in services such as `IModMenuService` and `IEventBus`.
## Define and share a contract
diff --git a/docs/modding/services.md b/docs/modding/services.md
index cf43e41..70a0573 100644
--- a/docs/modding/services.md
+++ b/docs/modding/services.md
@@ -5,7 +5,7 @@ StarMap provides core services and a registry for services shared between mods.
- [Constructor injection](constructor-injection.md)
- [Mod-provided services](mod-services.md) with `IModServiceRegistry`
- [In-game menu items](mod-menu.md) with `IModMenuService`
-- [Typed event mediation](event-mediation.md) with `IMediationService`
+- [Typed event bus](event-bus.md) with `IEventBus`
- [Method patching](patching.md) with `IPatchService`
For entry-point method signatures, see [lifecycle callbacks](lifecycle.md). For declaring mod dependencies and sharing assemblies, see [mod dependencies](dependencies.md).
diff --git a/mkdocs.yml b/mkdocs.yml
index f1d65f2..183ff7f 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -34,7 +34,7 @@ nav:
- Constructor injection: modding/constructor-injection.md
- Mod-provided services: modding/mod-services.md
- In-game menu items: modding/mod-menu.md
- - Typed event mediation: modding/event-mediation.md
+ - Typed event bus: modding/event-bus.md
- Method patching: modding/patching.md
- Dependencies: modding/dependencies.md
- Maintainers: maintainers.md