Kernelcom.zoa.networking.abstractions · v0.1.0

ZOA Networking Abstractions

Dependency-free networking contracts: protocol DTOs, session policy, adapter seams, and the backend support matrix.

This package depends on nothing, references no engine assembly, and contains no behaviour beyond validation and serialisation. Its whole job is to hold the vocabulary that the networking runtime, the domain packages, the build pipeline, and the operator tooling all have to agree on, without any of them depending on each other.

Three kinds of thing live here. First, protocol data: the versioned player command envelope, the session and threat and loot snapshots, the replicated event stream, and the bandwidth budget counters. Second, session policy: topology, trust tier, ruleset lock level, progression trust, connect tokens, and protocol compatibility. Third, the adapter seams that let the topology change without gameplay noticing: IDiscoveryAdapter, ITransportAdapter, IAuthAdapter, IAuthorityRuntime, IServerLicensePolicy, and ISessionProfile.

It also carries the authoritative backend support declaration. NetworkBackendSupportMatrix is the single file that decides which transports the 1.0 release promise covers, and both the runtime warning path and the Workbench tier labels read from it rather than from a duplicated list.

The package is safe to reference from anywhere because it is contract-only. The assembly is compiled with noEngineReferences and has no assembly references at all, so a headless server tool, an editor validator, and a gameplay system can share these types without any of them pulling in a Unity dependency.

Depends on (0)

Nothing. This is a root package.

How it works

Concepts

Commands are a deterministic wire format

PlayerCommandPayload is the canonical envelope for anything a player asks the authority to do. It carries a NetworkProtocolVersion, a Tick, a Sequence, a PlayerId, a PlayerCommandType of Movement, Look, Fire, Reload, Interact, or ReadyState, then the deterministic numeric fields MoveX, MoveY, LookYaw, and LookPitch, then the action flags FirePressed, ReloadRequested, InteractRequested, and Ready, then InteractTargetId and PlayerStateCode.

Serialize emits a stable pipe-delimited string using invariant culture and round-trip float formatting. TryDeserialize enforces exactly seventeen fields, parses every one with invariant culture, and rejects a command type byte that is not a defined enum value. The strictness is load-bearing: a format that tolerates a missing field desyncs quietly.

PlayerCommandValidator.Validate is the separate policy pass. It rejects a zero major protocol version, a zero PlayerId, a sequence that is not strictly greater than the last accepted one, any non-finite float, movement axes outside minus one to one, and an Interact command with a zero target id. It returns a PlayerCommandValidationResult with a reason string rather than throwing, because rejecting a command is a normal event on a server.

Snapshots and events reconstruct a late joiner

Session state is explicit and versioned rather than implied by whatever objects happen to be in the scene. SessionStateSnapshot carries the protocol version, CompletedTierCount, Score, IsVictory, and IsDefeat. ThreatTierSnapshot carries ActiveTier, ActiveWave, and WaveInProgress. LootStateSnapshot carries a LootId, IsClaimed, and the OwnerPlayerId that claimed it.

SessionReplicatedEvent is the ordered event stream on top of that base: a protocol version, a monotonic Sequence, a SessionEventType of TierActivated, WaveActivated, RewardSpawned, Defeat, Victory, or ScoreDelta, an IntValue, and an EntityId.

Late join is deterministic because of SessionStateReconstruction. OrderDeterministically sorts an event enumeration by Sequence, and Reconstruct applies the sorted events on top of a base snapshot. Two peers replaying the same base and the same events reach the same state regardless of arrival order, which is the property a reconnect needs.

The connect token is the join contract

ConnectToken is the opaque handoff between discovery and bootstrap, and it is the same shape for a Steam lobby join and a dedicated server join. It carries the ProviderId that minted it, an OpaquePayload the transport understands, the ProfileId the session runs, the SessionTopology, the NetworkProtocolVersion, the BuildVersion, plus IsModded and a ContentHash.

IsValid is a structural check, not a trust check: provider, payload, profile, and build version must all be present. TryCreate returns both the token and a reason string naming the missing field, so a discovery adapter can log what went wrong instead of returning a silent default.

DiscoverySessionRecord carries the same metadata on the browsing side, so an adapter can mint a token from a browse result without inventing values. IsModded and ContentHash travel end to end so that a mod-state or content mismatch is caught before the gameplay scene loads, not after a player is already in the world.

Compatibility resolves to a state

ProtocolCompatibility.Evaluate compares a client protocol version against the authority's and returns a ProtocolCompatibilityResult holding IsCompatible, a ProtocolCompatibilityState, and a player-facing reason.

The state enumeration makes each failure actionable: Compatible, InvalidToken, IncompatibleMajor, ClientTooOld, ServerTooOld, BuildVersionMismatch, ProfileMismatch, ModStateMismatch, and ContentHashMismatch. A client that is too old and a client running different mods need different messages and different remedies, so they are different states rather than one generic refusal.

ICompatibilityChecker is the richer seam for authorities that want to enforce more. It works over a CompatibilitySnapshot of BuildVersion, ContentChecksum, ProtocolVersion, IsModded, and ModManifestHash, which MultiplayerBuildManifest.ToCompatibilitySnapshot produces directly from the manifest baked into the build.

Session policy separates trust from topology

ISessionProfile pins five orthogonal decisions: SessionTopology of ListenHost or DedicatedServer, SessionTrustTier of HostLocal, CommunityDedicated, or CompetitiveDedicated, SessionRulesetLockLevel of Open, CommunityLocked, or CompetitiveLocked, whether mods are allowed, and a ProgressionTrustPolicy of LocalOnly, ServerLocal, or CompetitiveSigned.

Splitting trust from topology is the decision that makes one codebase serve both a free host mode and a paid dedicated mode. A listen host is cheap and socially trusted but is not a source of official truth, so its progression policy is LocalOnly. A dedicated server can enforce stronger authority and keep its own stats, so its policy is ServerLocal. Competitive results are signed.

SessionProfileIds pins the canonical strings free-host, community-dedicated, and competitive-dedicated, so a profile can be named in a connect token, a serialised asset, and a log line without three different spellings.

Adapters keep the platform out of gameplay

Four narrow interfaces carry every platform-specific concern. IDiscoveryAdapter turns a DiscoverySessionRecord into a ConnectToken and reports whether it is available at all. ITransportAdapter reports availability with an UnavailableReason and configures itself from a profile. IAuthAdapter validates a token against AuthEvidence and returns an AuthResult carrying an AuthDecision of Admit, Deny, or Retry. IAuthorityRuntime is the host or dedicated shell that owns simulation truth.

Gameplay code depends only on those. Whether the session is running on Steam lobbies, Steam game servers, a future EOS adapter, or a LAN transport is a bootstrap decision, and unit tests substitute fakes without any special harness.

The cost is real and worth naming: more interface surface and more bootstrap indirection than a prototype needs. The benefit is that adding a second store later is an adapter, not a rewrite.

Budgets are measured, not assumed

ReplicationBudgetSettings declares four numbers: PerEntityBytesPerSecond, PerSessionBytesPerSecond, SnapshotUpdatesPerSecond, and ReliableEventsPerSecond. They are targets, and something has to check them.

ReplicationBudgetTelemetry is that check. RecordSnapshotBytes accumulates per entity, RecordEventBytes accumulates per session and separately counts reliable events, and EvaluatePerSecond produces a ReplicationBudgetReport with IsWithinBudget, TotalBytesPerSecond, MaxEntityBytesPerSecond, ReliableEventsPerSecond, and a warning string naming what overran.

Keeping the counters in the contracts package rather than in a backend means the same budget evaluation runs in an edit-mode test, in a soak harness, and in a live session.

The support matrix is a single declaration

NetworkBackendSupportMatrix encodes one product decision in one file. FishNet is Supported. Netcode for GameObjects, Netick, Mirror, Fusion, and PUN 2 are Experimental. The local and null loopback backends are Internal. Anything else is Undeclared, which callers must treat as outside the promise.

Three consumers read it and none of them keep their own copy: the session runner warns when a session resolves outside the supported tier, the Workbench labels each backend row with TierLabel, and the release documentation generates its public support table from the same declaration. Promoting an adapter means editing this file, the release table, and the known-limits document in one change.

Describe returns the one-line support statement for a backend id, so a warning log or a Workbench tooltip says something useful instead of naming a tier.

Sync contracts describe what domain packages replicate

IZOANetworkSync is the base every replicating component implements: ComponentName, BytesSent, BytesReceived, IsOwner, IsServer, BackendId, and ResetCounters. It exists so the performance HUD can enumerate and graph traffic without knowing what any of those components actually are.

The specialised contracts describe the request-and-confirm shape of each domain. IZOANetworkHealthSync has RequestApplyDamage and SyncHealthState. IZOANetworkWeaponStateSync has RequestFire, RequestReload, RequestFireModeSwitch, RequestUnjam, and a full SyncWeaponState. IZOANetworkWeaponSync has RequestInstallAttachment and RequestDetachAttachment. IZOANetworkInventorySync has RequestMoveItem. There are matching contracts for loot, survival, and interactables.

Each of these is transport-agnostic and takes ids as strings rather than SDK handles, so an implementation can wrap whichever NetworkBehaviour the installed backend provides without leaking that type across the package boundary.

Setup

Workflow

  1. 01

    Reference it, do not fork it

    Any package that needs to name a topology, a profile, a protocol version, or a backend id references this assembly rather than declaring its own copy. It has no dependencies of its own and no engine references, so adding the reference costs nothing.

  2. 02

    Bump the protocol version when the wire shape changes

    Bump NetworkProtocolVersion when the command, snapshot, or event shape changes, and decide whether the change is major. A major bump makes ProtocolCompatibility.Evaluate return IncompatibleMajor and refuses the join outright, which is the correct outcome for a wire-format change.

  3. 03

    Validate before you apply

    On the authority, deserialise with TryDeserialize, then run PlayerCommandValidator.Validate carrying the last accepted sequence for that player. Treat a false result as a normal event to count, not an exception to propagate.

  4. 04

    Author profiles rather than booleans

    Express a new session mode as an ISessionProfile with an explicit trust tier, ruleset lock, mod permission, and progression policy. Adding a boolean to a bootstrap path is how host and dedicated behaviour drifts apart.

  5. 05

    Keep the support matrix truthful

    Promoting a backend from Experimental to Supported means a first-class INetworkingBackend implementation, smoke coverage in the release gates, and an edit to NetworkBackendSupportMatrix plus the public support table plus the known-limits document, all in one change.

Surface

Key types

PlayerCommandPayload

struct

The versioned player command envelope. Serialises to a stable pipe-delimited invariant-culture string and refuses to deserialise anything that is not exactly seventeen well-formed fields.

  • NetworkProtocolVersion ProtocolVersion, uint Tick, ushort Sequence, ulong PlayerId
  • PlayerCommandType CommandType
  • float MoveX, MoveY, LookYaw, LookPitch
  • bool FirePressed, ReloadRequested, InteractRequested, Ready
  • ulong InteractTargetId, byte PlayerStateCode
  • string Serialize()
  • static bool TryDeserialize(string value, out PlayerCommandPayload payload)

PlayerCommandValidator

class

Deterministic bounds and sequencing check for a command, run by the authority before it is applied. Returns a reason rather than throwing.

  • static PlayerCommandValidationResult Validate(PlayerCommandPayload payload, ushort? lastAcceptedSequence = null)

PlayerCommandType

enum

Movement, Look, Fire, Reload, Interact, ReadyState. Serialised as a byte on the wire.

NetworkProtocolVersion

struct

Explicit major, minor, and patch used for every compatibility check. NetworkProtocolVersion.Initial is 1.0.0 and is the default authority version.

ConnectToken

struct

The discovery-to-bootstrap handoff, identical in shape for lobby and dedicated joins. TryCreate names the missing field when construction fails.

  • string ProviderId, OpaquePayload, ProfileId, BuildVersion, ContentHash
  • SessionTopology Topology, NetworkProtocolVersion ProtocolVersion, bool IsModded
  • bool IsValid { get; }
  • static bool TryCreate(..., out ConnectToken token, out string reason)

DiscoverySessionRecord

struct

Browse-side metadata for a discovered session: session id and name, profile id, topology, protocol version, opaque payload, build version, mod state, and content hash.

ProtocolCompatibility

class

Evaluates a client protocol version against the authority's and produces an actionable result.

  • static ProtocolCompatibilityResult Evaluate(NetworkProtocolVersion client, NetworkProtocolVersion authority)

ProtocolCompatibilityState

enum

Compatible, InvalidToken, IncompatibleMajor, ClientTooOld, ServerTooOld, BuildVersionMismatch, ProfileMismatch, ModStateMismatch, ContentHashMismatch. Distinct states because each one implies a different remedy.

ISessionProfile

interface

Immutable session policy: topology, trust tier, ruleset lock level, mod permission, and progression trust policy, plus a stable ProfileId and DisplayName.

SessionTopology

enum

ListenHost or DedicatedServer. The runtime shape of the session, kept separate from how much it is trusted.

SessionTrustTier

enum

HostLocal, CommunityDedicated, CompetitiveDedicated. How much a session's results are believed.

ProgressionTrustPolicy

enum

LocalOnly, ServerLocal, CompetitiveSigned. Declares where progression from a session may be written and whether it needs a signature.

SessionProfileIds

class

The canonical profile id strings: free-host, community-dedicated, competitive-dedicated.

IAuthorityRuntime

interface

The host or dedicated shell that owns simulation truth. A tiny surface: an id, a protocol version, an IsRunning flag, Start with a profile, and Stop.

IDiscoveryAdapter

interface

Session browser seam. Reports availability and turns a DiscoverySessionRecord into a ConnectToken with a reason on failure.

  • string AdapterId { get; }
  • bool IsAvailable { get; }
  • bool TryCreateConnectToken(DiscoverySessionRecord sessionRecord, out ConnectToken token, out string reason)

ITransportAdapter

interface

Transport seam selected at bootstrap. Exposes an UnavailableReason so a fallback can explain itself, and configures from the resolved profile.

  • string AdapterId { get; }
  • bool IsAvailable { get; }
  • string UnavailableReason { get; }
  • void Configure(ISessionProfile profile)

IAuthAdapter

interface

Join-auth seam run before admission. Validates a ConnectToken against AuthEvidence and returns an AuthResult carrying Admit, Deny, or Retry plus a reason.

INetworkBackend

interface

Diagnostics-level view of the active networking system: backend id, display name, NetworkBackendKind, NetworkRole, IsRunning, IsConnected, and a never-null Diagnostics string.

INetworkBackendProvider

interface

Factory and descriptor for a backend, discovered at runtime so integrations can live in separate packages. Reports a NetworkBackendAvailability with a reason before anything is created.

  • NetworkBackendAvailability Availability { get; }
  • INetworkBackend CreateBackend()

NetworkBackendSupportMatrix

class

The single authoritative declaration of the 1.0 backend support promise. Every warning, Workbench label, and release table reads from here.

  • const string SupportedBackendId
  • static NetworkBackendSupportTier GetTier(string backendId)
  • static bool IsSupported(string backendId)
  • static string TierLabel(string backendId)
  • static string Describe(string backendId)
  • static IReadOnlyCollection<string> DeclaredBackendIds { get; }

NetworkBackendSupportTier

enum

Undeclared, Supported, Experimental, Internal. Undeclared and Experimental both sit outside the release promise; Internal marks loopback infrastructure that is never shippable.

NetworkBackendIds

class

Stable backend id strings: local, netcode, netick, mirror, fusion, pun2, fishnet.

NetworkTransportIds

class

Stable transport adapter ids: fishy-steamworks and local-loopback.

NetworkRole

enum

None, Client, Server, Host. The local process's role in the current topology.

SessionStateSnapshot

struct

Top-level session progression: protocol version, CompletedTierCount, Score, IsVictory, IsDefeat. The base a late joiner is reconstructed from.

SessionReplicatedEvent

struct

One ordered authority event: protocol version, Sequence, SessionEventType, IntValue, EntityId. Deterministic replay depends on Sequence.

SessionStateReconstruction

class

Deterministic late-join replay. Orders events by sequence and folds them onto a base snapshot, so two peers replaying the same inputs agree.

  • static IReadOnlyList<SessionReplicatedEvent> OrderDeterministically(IEnumerable<SessionReplicatedEvent> events)
  • static SessionStateSnapshot Reconstruct(...)

ReplicationBudgetTelemetry

class

Per-entity and per-session byte counters plus reliable event counters, evaluated against a budget into a warning report.

  • void RecordSnapshotBytes(ulong entityId, int bytes)
  • void RecordEventBytes(int bytes, bool reliable)
  • ReplicationBudgetReport EvaluatePerSecond(...)
  • void Reset()

ServerLicense

class

The immutable capability artefact for a dedicated server: issuer, subject, issue date, optional expiry, allowed profiles, slot cap, and signature.

ILicenseVerifier

interface

Verifies a licence signature, expiry, and structure. Implementations must not require network access; verification is fully offline.

ILicenseCapabilityGate

interface

The integration point between licensing and the server runtime. Answers whether the competitive profile can activate, what the licensed slot cap is, and whether a given profile may run.

RulesetHash

struct

Deterministic hash over gameplay-affecting competitive settings, computed from a parameter array. Competitive sessions publish it so a match can be validated against the rules it claimed to run.

MatchAuditBundle

class

The exportable, signable record of a completed match: bundle and match id, profile, protocol version, ruleset hash, result summary, competitive flag, timestamps, player counts, and a signature over GetSignablePayload.

IZOANetworkSync

interface

Base contract for any component that reports its own replication traffic. A component appears in the network performance HUD by implementing it.

  • string ComponentName { get; }
  • ulong BytesSent { get; } / ulong BytesReceived { get; }
  • bool IsOwner { get; } / bool IsServer { get; }
  • string BackendId { get; }
  • void ResetCounters()

IAuthorityCommandValidator

interface

Authority-side exploit rejection over fire rate, movement, and interaction distance. Records violations per player and reports when accumulated violations warrant a disconnect.

IConnectionAbuseController

interface

Rate limiting for repeated bad passwords and malformed handshakes, configurable per session profile, with a per-endpoint failure window that resets on a successful join.

MatchArchetype

enum

CoOpPvE, deathmatch PvP, team PvP, and sandbox. Bootstrap resolves an archetype alongside a topology, and archetype-specific behaviour hangs off IScoreService, ITeamService, and ISpawnService rather than scene-wide conditionals.

MultiplayerBuildManifest

struct

Build metadata embedded in every multiplayer artifact: build version, commit hash, product, content checksum, protocol version, and timestamp. Converts directly into the CompatibilitySnapshot used at admission.

  • CompatibilitySnapshot ToCompatibilitySnapshot(bool isModded = false, string modManifestHash = null)

MultiplayerBuildTargetCatalog

class

The canonical build targets: retail client, dedicated server, server manager, and support console, each with an artifact id, a MultiplayerBuildFeatureSet of Retail or InternalTooling, headless compatibility, and default launch arguments.

Usage

Examples

Round-tripping and validating a commandcsharp
using ZOA.Networking.Abstractions;

var command = new PlayerCommandPayload(
    protocolVersion: NetworkProtocolVersion.Initial,
    tick: 512,
    sequence: 41,
    playerId: 7,
    commandType: PlayerCommandType.Movement,
    moveX: 0.5f,
    moveY: -1f,
    lookYaw: 132.5f,
    lookPitch: -8f,
    firePressed: false,
    reloadRequested: false,
    interactRequested: false,
    interactTargetId: 0,
    ready: true,
    playerStateCode: 1);

var wire = command.Serialize();

// The authority side.
if (!PlayerCommandPayload.TryDeserialize(wire, out var received))
    return; // Wrong field count, bad culture, or an undefined command type.

var verdict = PlayerCommandValidator.Validate(received, lastAcceptedSequence);
if (!verdict.IsValid)
{
    // verdict carries the reason: out-of-range movement, non-finite
    // floats, a replayed sequence, or a zero interact target.
    RecordRejectedCommand(received.PlayerId, verdict);
    return;
}

lastAcceptedSequence = received.Sequence;
Sequence is checked against the last accepted value, so a replayed packet is rejected without any extra bookkeeping at the call site.
Reconstructing state for a late joinercsharp
using ZOA.Networking.Abstractions;

// The authority holds a base snapshot plus every event it has issued.
var ordered = SessionStateReconstruction.OrderDeterministically(replicatedEvents);
var current = SessionStateReconstruction.Reconstruct(baseSnapshot, ordered);

// current now reflects tier completions, score deltas, and any
// victory or defeat that landed, in sequence order rather than in
// whatever order the events happened to arrive.
SendToJoiningPeer(current, ordered);
Minting and checking a connect tokencsharp
using ZOA.Networking.Abstractions;

if (!ConnectToken.TryCreate(
        providerId: "steam-lobby-discovery",
        opaquePayload: lobbyId,
        profileId: SessionProfileIds.FreeHost,
        topology: SessionTopology.ListenHost,
        protocolVersion: NetworkProtocolVersion.Initial,
        buildVersion: buildManifest.BuildVersion,
        isModded: buildManifest.ContentChecksum != vanillaChecksum,
        contentHash: buildManifest.ContentChecksum,
        out var token,
        out var reason))
{
    // reason names the missing field: provider, payload, profile, or build.
    Debug.LogWarning(reason);
    return;
}

var compatibility = ProtocolCompatibility.Evaluate(
    token.ProtocolVersion,
    authorityProtocolVersion);

if (compatibility.State == ProtocolCompatibilityState.ClientTooOld)
    PromptForUpdate(compatibility.Reason);
Reading the support matrixcsharp
using ZOA.Networking.Abstractions;

foreach (var backendId in NetworkBackendSupportMatrix.DeclaredBackendIds)
{
    var tier = NetworkBackendSupportMatrix.GetTier(backendId);
    var label = NetworkBackendSupportMatrix.TierLabel(backendId);

    // "SUPPORTED", "EXPERIMENTAL", "INTERNAL", or "UNDECLARED".
    AddRow(backendId, label, NetworkBackendSupportMatrix.Describe(backendId));
}

if (!NetworkBackendSupportMatrix.IsSupported(activeBackendId))
{
    // An unknown id returns Undeclared, which callers must treat the
    // same as unsupported rather than assuming it is fine.
    WarnOutsideSupportPromise(activeBackendId);
}
Tracking a replication budgetcsharp
using ZOA.Networking.Abstractions;

var budget = new ReplicationBudgetSettings(
    perEntityBytesPerSecond: 2048,
    perSessionBytesPerSecond: 65536,
    snapshotUpdatesPerSecond: 30,
    reliableEventsPerSecond: 20);

var telemetry = new ReplicationBudgetTelemetry();

// Per snapshot write.
telemetry.RecordSnapshotBytes(entityId, payload.Length);

// Per reliable authority event.
telemetry.RecordEventBytes(eventBytes, reliable: true);

// Once a second.
var report = telemetry.EvaluatePerSecond(budget, elapsedSeconds);
if (!report.IsWithinBudget)
    Debug.LogWarning(report.Warning);

telemetry.Reset();

Read this

Notes and caveats

See also