Kernelcom.zoa.messaging · v0.4.0

ZOA Messaging

The publish/subscribe bus and service registry every other package talks through.

Messaging is the root of the dependency graph. It depends on nothing, and almost everything depends on it. When a weapon needs to tell the HUD it reloaded, it publishes an event rather than holding a reference to the HUD, and the two packages never learn about each other.

There are two primitives here and they do different jobs. The event bus carries things that happened. The service registry answers the question "who is providing X right now". Reaching for the wrong one is the most common mistake. If a subscriber missing the message is fine, publish it; if the caller needs an answer, resolve a service.

Both are small. There is no DI container, no reflection scan, and no attribute magic. The registry is a static type-keyed locator; the bus dispatches synchronously, in process.

Depends on (0)

Nothing. This is a root package.

How it works

Concepts

The bus is hosted for you

FoundryEventBusHost spawns itself before the first scene loads, creates an InProcessFoundryEventBus, registers it, and marks itself DontDestroyOnLoad. You never place it in a scene and there is no prefab to remember.

The one ordering constraint worth knowing: if your own code runs at RuntimeInitializeLoadType.BeforeSceneLoad and publishes immediately, the host may not have registered yet. Use AfterSceneLoad, or defer to Start.

Cross-cutting contracts live here

A handful of contracts sit in Messaging that look like they belong elsewhere. AudioBusId and IAudioBusService are the clearest case: both Nucleon (player audio) and the AI package need them, and putting them in either one would couple Nucleon to AI.

Messaging is the sanctioned home for a contract that more than one package needs and no package owns. Break that rule and the graph grows edges nobody intended.

Surface

Key types

IFoundryEventBus

interface

Publish/subscribe over plain CLR types. Publishing to nobody is legal and free.

  • void Publish<T>(in T message)
  • IDisposable Subscribe<T>(Action<T> handler)

FoundryServiceRegistry

service

Static, type-keyed service locator. Register an implementation, resolve it from anywhere, without a container.

  • static void Register<T>(T instance)
  • static bool TryResolve<T>(out T instance)
  • static T Get<T>()

InProcessFoundryEventBus

class

The default in-memory bus implementation. Synchronous dispatch on the calling thread.

FoundryEventBusHost

component

Hidden bootstrapper that owns the scene-lifetime bus. Auto-spawned, DontDestroyOnLoad, execution order -10000. Not added by hand.

IPlayerContext

interface

Resolves the active player without a scene reference, so packages can ask 'who is the player' without depending on Nucleon.

IAudioBusService

interface

Resolves AudioBusId values to AudioMixerGroups and exposes per-bus volume. Linear 0-1 at the surface, converted to dB internally.

  • AudioMixerGroup GetGroup(AudioBusId bus)
  • void SetVolume(AudioBusId bus, float linear01)
  • float GetVolume(AudioBusId bus)
  • event Action<AudioBusId, float> VolumeChanged

AudioBusId

enum

Master, Music, SFX, Voice, Ambient. Gameplay code targets a sub-bus; Master is for settings sliders.

CursorLockBus

class

Arbitrates cursor lock between systems that each think they should own it, so opening inventory over a locked first-person camera behaves.

Usage

Examples

Publishing an eventcsharp
using ZOA.Messaging;

public readonly struct WeaponReloaded
{
    public readonly int WeaponId;
    public readonly int RoundsLoaded;

    public WeaponReloaded(int weaponId, int roundsLoaded)
    {
        WeaponId = weaponId;
        RoundsLoaded = roundsLoaded;
    }
}

// Anywhere that finishes a reload:
var bus = FoundryServiceRegistry.Get<IFoundryEventBus>();
bus.Publish(new WeaponReloaded(weaponId, 30));
Events are plain structs. Passing by in avoids a copy and signals that the message is read-only.
Subscribing, and unsubscribing properlycsharp
using System;
using UnityEngine;
using ZOA.Messaging;

public sealed class AmmoCounter : MonoBehaviour
{
    private IDisposable _subscription;

    private void OnEnable()
    {
        var bus = FoundryServiceRegistry.Get<IFoundryEventBus>();
        _subscription = bus.Subscribe<WeaponReloaded>(OnReloaded);
    }

    // Subscribe returns IDisposable rather than needing a matching
    // Unsubscribe call, so the teardown cannot drift from the setup.
    private void OnDisable() => _subscription?.Dispose();

    private void OnReloaded(WeaponReloaded evt) => Redraw(evt.RoundsLoaded);

    private void Redraw(int rounds) { /* ... */ }
}
Registering a servicecsharp
using ZOA.Messaging;
using ZOA.Messaging.Audio;

public sealed class ProjectAudioBusService : IAudioBusService
{
    // ... implementation ...
}

// During bootstrap, before anything resolves it:
FoundryServiceRegistry.Register<IAudioBusService>(new ProjectAudioBusService());

// From anywhere afterwards:
if (FoundryServiceRegistry.TryResolve<IAudioBusService>(out var audio))
    audio.SetVolume(AudioBusId.SFX, 0.8f);
Prefer TryResolve where a missing service is survivable. Get throws, and during bootstrap a missing service should be loud.

Read this

Notes and caveats

See also