Skip to content

08 — Authority

Authority is who gets to decide. Lattice distinguishes two kinds:

  • State authority — the peer that owns and writes the simulation. In Server/Host modes this is the server; it's the only peer that may Spawn, WriteState, and MarkDirty.
  • Input authority — the peer whose input drives a particular object (its Owner). A client can have input authority over its own avatar without having state authority over the world.

Verified against: bindings/lattice-unity/com.lattice.netcode/Runtime/NetworkObject.cs:56-72, NetworkRunner.cs:98-99.

Reading authority off an object
obj.HasStateAuthority   // true if the local runner is server/host
obj.HasInputAuthority   // true if obj.Owner == runner.LocalPlayer
obj.Owner               // == obj.InputAuthority — the participant that owns it
runner.IsAuthority      // true for Server / Host / SharedHost

Verified against: NetworkObject.cs:62-72, NetworkRunner.cs:98-99.

What LocalPlayer is, and when it is not known

HasInputAuthority is only as good as LocalPlayer. An authority is peer 0; a client is told its id during the secure handshake and reports LatticeNative.LocalPeerUnknown until then, so HasInputAuthority is simply never true on a plaintext client. Check runner.IsLocalPlayerKnown before relying on it — see Peer identity & local player.

Host-authoritative validation (the sample's model)

The most important authority pattern — and the one the sample verifies end to end — is server-authoritative validation: clients request changes; the authority validates and applies them; illegal requests change nothing.

In tic-tac-toe the host is the sole writer of the board. A client never writes the board directly — it sends a Move event, and the host decides:

RequestMove — client asks, host applies (from the sample)
public void RequestMove(int cell)
{
    if (_board == null) return;
    if (IsHost)
        TryApplyMove(_runner.LocalPlayer, LocalMark, cell);           // host applies its own move directly
    else
        _runner.SendEvent(Net.EventMove, LatticeEventTarget.Server,   // client asks the host
                          Payloads.Move(cell), reliable: true);
}

Verified against: TicTacToeSession.cs:106-113.

The host's authoritative handler validates against the pure rules, and only on success writes the state, flips the turn, re-evaluates the result, and marks the changed fields dirty so they replicate. An illegal move changes no state and is surfaced via OnMoveRejected:

TryApplyMove — the authority's decision (from the sample)
private bool TryApplyMove(ulong fromPeer, Mark mover, int cell)
{
    var state = _board.ReadState<BoardState>();
    byte[] cells = state.ToCells();

    if (!TicTacToeRules.IsLegalMove(cells, (Mark)state.turn, (GameResult)state.result, mover, cell))
    {
        OnMoveRejected?.Invoke(fromPeer, cell);   // wrong turn / taken cell / game over
        return false;
    }

    state.SetCell(cell, (byte)mover);
    cells[cell] = (byte)mover;

    GameResult result = TicTacToeRules.Evaluate(cells);
    state.result = (byte)result;
    state.turn   = (byte)(TicTacToeRules.IsOver(result) ? Mark.Empty : TicTacToeRules.Other(mover));

    _board.WriteState(state);
    _board.MarkDirty(BoardField.ForCell(cell));
    _board.MarkDirty(BoardField.Turn);
    _board.MarkDirty(BoardField.Result);

    OnBoardChanged?.Invoke(state);   // host's own view updates immediately
    return true;
}

Verified against: TicTacToeSession.cs:141-165; the rules at TicTacToeRules.cs:51-58,64-78.

Crucially, the mover's mark is the one the host assigned to that peer — not one the client claims — using the sender id from the event (chapter 07):

Host maps the sender to its seat, then validates
private void HandleMoveEvent(ulong sender, byte[] payload)
{
    if (_board == null) return;
    AssignMarkIfNeeded(sender);                                       // tolerate move-before-join
    Mark mover = _peerMarks.TryGetValue(sender, out var m) ? m : Mark.Empty;
    TryApplyMove(sender, mover, Payloads.ReadMoveCell(payload));      // authority decides
}

Verified against: TicTacToeSession.cs:218-224.

This is verified

The headless harness asserts the authority rejects moving out of turn, taking an occupied cell, and moving after the game is over, leaving the board unchanged on both peers — while legal moves replicate. This is the core guarantee of a server-authoritative game.
Verified against: samples/unity-tictactoe/headless/Program.cs:84-105,134-138; samples/unity-tictactoe/README.md ("an illegal move is rejected by the authority").

The recipe

  1. Client sends a request (event or RPC to Server).
  2. Authority validates against its rules using the trusted sender identity.
  3. On success: WriteState + MarkDirty → it replicates. On failure: change nothing, optionally notify.
  4. Clients only ever read replicated state and render it.

This keeps a cheating client from writing illegal state: it can only ask, and the authority is the single writer.

Distributed / shared authority

For games where authority over an object should move between peers (e.g. whoever grabbed the ball owns it), Lattice has a shared / distributed authority model. The API:

Shared-authority API (runner + object)
runner.RequestAuthority(ulong netid);   // ask the host arbiter for state authority over an object
runner.ObjectOwner(ulong netid);        // who currently holds authority (0 if unknown)
runner.ObjectAuthorityTick(ulong netid);// the token's claim counter

// object-scoped equivalents:
obj.RequestAuthority();
obj.CurrentOwner;
obj.AuthorityTick;

Verified against: NetworkRunner.cs:354-365, NetworkObject.cs:149-164.

RequestAuthority is sent reliably to the host arbiter, which mints a new ownership token and broadcasts the grant. Once observed, OnAuthorityChanged fires on every peer the tick it adopts the new token, and the requester's writes win:

React to an authority grant
runner.OnAuthorityChanged += (netid, newOwner, authorityTick) =>
{
    // newOwner now holds state authority over netid, as of authorityTick.
};

Verified against: NetworkRunner.cs:35-37 (event + doc), NetworkRunner.cs:160-161 (trampoline), LatticeNative.cs:196-200.

This path is intended for SharedHost (LatticeGameMode.SharedHost) sessions.

Real ABI, but not exercised by the sample or the headless test

RequestAuthority / OnAuthorityChanged / ObjectOwner / ObjectAuthorityTick are genuine wrappers over the core's lattice_request_authority / on_authority_changed / lattice_object_owner / lattice_object_authority_tick. The tic-tac-toe sample is single-authority (host-only), so it does not call them, and the binding's headless test covers spawn/replicate/RPC/despawn but not an authority transfer. The API surface and its documented semantics are accurate; end-to-end transfer isn't demonstrated in this tutorial's verified sources. Design against it for shared-authority games, and test the transfer in your own integration.

Honesty note: AssignInputAuthority is a stub

The Fusion-parity method NetworkObject.AssignInputAuthority(player) does not replicate. It updates the local Owner view only — the core has no input-authority transfer ABI (only state authority via RequestAuthority). Don't rely on it to move ownership across the network.

NetworkObject.AssignInputAuthority — COMPILE-STUB
public void AssignInputAuthority(ulong player) => Owner = player;   // local only; does not replicate

Verified against: NetworkObject.cs:86-91 (documented as a compile-stub).


Next: 09 — Full walkthrough, where all of this comes together into the complete game.